自学内容网 自学内容网

从C#中的结构体和类的区别中看引用和值的问题

在 C#中,结构体(struct)和类(class)都是用于创建自定义数据类型的方式,但它们在很多方面存在着显著的区别。掌握他们的区别至少不会产生一些我们不了解情况下发生的错误。

一、作为参数传递时的差别

1. 类参数

  • 当类的实例对象作为参数传递给一个方法时,传递的是对象的引用。这意味着在方法内部对参数对象的修改会影响到原始对象。
  • 示例代码:
class MyClass
{
    public int Value { get; set; }
}

class Program
{
    static void ModifyClassObject(MyClass obj)
    {
        obj.Value = 20;
    }
    static void Main()
    {
        MyClass classInstance = new MyClass();
        classInstance.Value = 10;
        ModifyClassObject(classInstance);
        Console.WriteLine($"Class - Value after modification: {classInstance.Value}");
        // 输出为20,因为在ModifyClassObject方法中修改了对象的属性,而传递的是引用
    }
}

2. 结构体参数

  • 当结构体的实例作为参数传递给一个方法时,传递的是结构体的副本。这意味着在方法内部对参数结构体的修改不会影响到原始结构体。
  • 示例代码:
struct MyStruct
{
    public int Value;
}

class Program
{
    static void ModifyStructObject(MyStruct structObj)
    {
        structObj.Value = 20;
    }
    static void Main()
    {
        MyStruct structInstance = new MyStruct();
        structInstance.Value = 10;
        ModifyStructObject(structInstance);
        Console.WriteLine($"Struct - Value after modification: {structInstance.Value}");
        // 输出为10,因为在ModifyStructObject方法中修改的是副本,原始结构体没有被修改
    }
}

二、内存分配上的区别

1. 类(引用类型)

  • 类的对象实例存储在堆(Heap)内存中。堆是一个用于动态分配内存的区域,由垃圾回收器(Garbage Collector)管理。当创建一个类的对象时,会在堆中分配足够的内存来存储对象的所有成员(字段、属性等)。变量(引用)本身存储在栈(Stack)中,它包含了对象在堆中的内存地址,通过这个引用可以访问堆中的对象。
  • 示例代码:
class MyClass
{
    public int Value;
}

class Program
{
    static void Main()
    {
        MyClass obj = new MyClass();
        // 这里,obj是一个引用,存储在栈中,它指向堆中实际的MyClass对象
        // 堆中的MyClass对象存储了Value字段
    }
}

2. 结构体(值类型)

  • 结构体的实例通常存储在栈(Stack)中。栈是一种后进先出(LIFO)的数据结构,用于存储局部变量和方法调用的信息等。当定义一个结构体变量时,如果它是一个局部变量或者作为另一个值类型的成员,它会在栈中分配内存来存储其成员。然而,如果结构体作为一个引用类型的成员(如类的字段),那么结构体实例会跟随引用类型一起存储在堆中。
  • 示例代码:
struct MyStruct
{
    public int Value;
}

class Program
{
    static void Main()
    {
        MyStruct structObj;
        // structObj是一个结构体,存储在栈中,直接包含Value成员
    }
}

三、使用场景

1. 使用结构体的情况

  • 数据简单且占用空间小:当需要表示简单的数据集合,如点坐标(xy)、矩形的尺寸(widthheight)等,结构体是一个很好的选择。因为结构体在内存中布局紧凑,对于小型数据结构可以减少内存开销。
  • 按值传递语义需求:如果希望数据在传递过程中(如作为参数传递给方法或者赋值给其他变量)是按值复制的,以确保数据的独立性,那么结构体符合要求。例如,在一些数学计算或者图形处理的函数中,不希望函数内部对参数的修改影响到原始数据。
  • 性能敏感且无继承需求:在对性能要求较高的场景中,特别是涉及大量数据的复制和操作,结构体的性能可能更优(因为栈内存的访问速度通常比堆内存快)。并且如果不需要继承和多态的功能,结构体可以满足需求。

2. 使用类的情况

  • 需要引用语义:当希望多个变量引用同一个对象实例,并且通过任何一个引用修改对象都会影响到其他引用所指向的对象时,应该使用类。例如,在对象之间共享状态或者实现观察者模式等场景。
  • 复杂的业务逻辑和行为:如果数据类型需要包含复杂的方法、属性访问逻辑、事件等,类提供了更好的组织和封装方式。通过将数据和操作封装在类中,可以更好地实现面向对象的设计原则。
  • 继承和多态需求:如果需要构建一个类型层次结构,通过继承来共享公共的属性和行为,并且利用多态来实现不同子类的特定行为,那么必须使用类。例如,在图形绘制系统中,有Shape基类,以及CircleRectangle等派生类,可以通过继承和多态来统一处理不同形状的绘制操作。

四、对结构体实现引用的操作

1. 使用ref关键字

  • 在 C#中,可以使用ref关键字来实现将结构体作为引用传递。这样,在方法内部对结构体的修改就会影响到原始的结构体。
  • 示例代码:
struct Point
{
    public int X;
    public int Y;
}
class Program
{
    static void ModifyPoint(ref Point p)
    {
        p.X = 100;
        p.Y = 100;
    }
    static void Main()
    {
        Point originalPoint = new Point();
        originalPoint.X = 0;
        originalPoint.Y = 0;
        ModifyPoint(ref originalPoint);
        Console.WriteLine($"Original Point: X = {originalPoint.X}, Y = {originalPoint.Y}");
        // 输出为Original Point: X = 100, Y = 100,因为使用了ref关键字,修改影响了原始结构体
    }
}

2. 使用in关键字(用于只读引用)

  • in关键字也可以用于传递结构体的引用,但它表示传递的是一个只读引用。这意味着在方法内部不能修改结构体的成员。这种方式在需要将结构体传递给方法进行访问,但又不希望方法修改结构体的情况下很有用。
  • 示例代码:
struct Point
{
    public int X;
    public int Y;
}
class Program
{
    static void PrintPoint(in Point p)
    {
        Console.WriteLine($"Point: X = {p.X}, Y = {p.Y}");
        // p.X = 100;  // 这行代码会报错,因为in关键字表示只读引用
    }
    static void Main()
    {
        Point originalPoint = new Point();
        originalPoint.X = 0;
        originalPoint.Y = 0;
        PrintPoint(in originalPoint);
    }
}

3. 将结构体包装在类中(间接引用)

  • 另一种实现对结构体引用的方式是将结构体作为类的一个成员字段,然后通过类的引用来操作结构体。
  • 示例代码:
struct Point
{
    public int X;
    public int Y;
}
class PointContainer
{
    public Point ThePoint;
}
class Program
{
    static void ModifyPointInContainer(PointContainer container)
    {
        container.ThePoint.X = 100;
        container.ThePoint.Y = 100;
    }
    static void Main()
    {
        PointContainer container = new PointContainer();
        container.ThePoint = new Point();
        container.ThePoint.X = 0;
        container.ThePoint.Y = 0;
        ModifyPointInContainer(container);
        Console.WriteLine($"Point in container: X = {container.ThePoint.X}, Y = {container.ThePoint.Y}");
        // 输出为Point in container: X = 100, Y = 100,通过类引用修改了结构体成员
    }
}

五、防止类作为参数被修改的操作

1. 使用接口和不可变类型(推荐)

  • 定义接口
    • 首先,定义一个接口来描述类的行为。接口只包含方法签名,没有具体的实现,这样可以限制对类的访问和操作。
    • 例如,假设有一个Person类,定义一个IPersonReadOnly接口来提供只读方法:
interface IPersonReadOnly
{
    string GetName();
    int GetAge();
}
class Person : IPersonReadOnly
{
    private string name;
    private int age;
    public Person(string name, int age)
    {
        this.name = name;
        this.age = age;
    }
    public string GetName()
    {
        return name;
    }
    public int GetAge()
    {
        return age;
    }
}
  • 传递接口作为参数
    • 当传递Person类的实例作为参数时,将其作为IPersonReadOnly接口类型传递。这样,接收参数的方法只能调用接口中定义的只读方法,无法修改类的内部状态。
    • 示例代码如下:
class Program
{
    static void PrintPersonInfo(IPersonReadOnly person)
    {
        Console.WriteLine($"Name: {person.GetName()}, Age: {person.GetAge()}");
    }
    static void Main()
    {
        Person person = new Person("John", 30);
        PrintPersonInfo(person);
    }
}
  • 这种方式的优点是通过接口的抽象,强制实现了只读访问,符合面向对象设计中的依赖倒置原则,使得代码更加灵活和可维护。同时,Person类本身可以是不可变的(如果不提供修改属性的方法),进一步确保了数据的稳定性。

2 .使用in关键字(C# 7.2及以上)

  • in关键字用于传递参数的只读引用。它类似于ref关键字,但不允许在方法内部修改引用的对象。
  • 例如,对于Person类:
class Person
{
    public string Name { get; }
    public int Age { get; }
    public Person(string name, int age)
    {
        Name = name;
        Age = age;
    }
}
class Program
{
    static void PrintPersonInfo(in Person person)
    {
        Console.WriteLine($"Name: {person.Name}, Age: {person.Age}");
        // person.Name = "New Name";  // 这行代码会报错,因为in关键字表示只读引用
    }
    static void Main()
    {
        Person person = new Person("Alice", 25);
        PrintPersonInfo(person);
    }
}
  • 使用in关键字可以明确表示参数是只读的,编译器会防止在方法内部意外地修改对象。不过需要注意的是,in关键字传递的仍然是引用,只是禁止了修改操作。如果对象是可变的并且在方法调用之外被修改,这些变化在方法内部是可见的(因为它是引用类型)。

3. 传递副本(如果适用)

  • 如果类实现了ICloneable接口或者有合适的复制构造函数,可以考虑传递类的副本而不是原始引用。
  • 例如,假设Person类实现了ICloneable接口:
class Person : ICloneable
{
    public string Name { get; set; }
    public int Age { get; set; }
    public object Clone()
    {
        return new Person(Name, Age);
    }
}
class Program
{
    static void PrintPersonInfo(Person person)
    {
        Console.WriteLine($"Name: {person.Name}, Age: {person.Age}");
        // 在这里修改person不会影响原始对象
        person.Name = "New Name";
    }
    static void Main()
    {
        Person person = new Person("Bob", 35);
        PrintPersonInfo((Person)person.Clone());
        Console.WriteLine($"Original Name: {person.Name}");
    }
}

这种方法的缺点是复制对象可能会带来性能开销,特别是对于复杂的对象或者对象图。并且如果对象的复制逻辑比较复杂(例如,对象包含引用其他对象的字段,需要进行深度复制),实现起来可能会比较困难。因此,这种方法通常在对象比较简单或者对性能要求不高的情况下使用。

总结

在 C#中,结构体和类有着多方面的区别且各有其适用场景。

在作为参数传递时,类传递的是引用,方法内对参数的修改会影响原始对象;而结构体传递的是副本,方法内的修改不会作用于原始结构体。在内存分配方面,类对象实例存于堆内存,通过栈中的引用访问,结构体实例通常在栈中(特殊情况除外)。

使用场景上,结构体适合表示简单、小型且无需继承和多态的数据集合,按值传递保证数据独立以及对性能要求较高的情况;类则用于需要引用语义、有着复杂业务逻辑和行为以及构建继承、多态的类型层次结构的场景。

对于结构体实现引用,可以借助 ref 关键字实现可修改的引用传递,用 in 关键字实现只读引用传递,或者将结构体包装在类中进行间接引用操作。而针对不想类在作为参数传递时被修改的情况,可通过定义接口传递接口类型实现只读访问、利用 in 关键字禁止方法内修改,或者传递副本(前提是类支持合适的复制机制)来达成目的。

理解结构体和类的这些区别以及对应的操作方法,有助于开发者根据项目实际需求合理选择使用结构体或类,编写出更高效、更符合设计要求的 C#代码,从而优化程序的性能、可维护性以及逻辑的正确性。


原文地址:https://blog.csdn.net/haigear/article/details/144306921

免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!