Skip to content
On this page

结构

结构体(Struct)是一种值类型(Value Type),它可以包含不同类型的成员,例如字段、属性、方法和有参构造函数等成员,类似于类(Class),这些成员可以像类一样进行访问和使用。

与类不同的是,结构体是一种轻量级的类型,它被设计为适合用于存储小型数据结构的情况,例如坐标、颜色、日期等。

  • 结构体类型通过使用 struct 关键字进行声明。

    csharp
    // 定义了一个名为 `Point` 的结构体
    // 它有两个整型的成员 `X` 和 `Y`
    // 并定义了一个构造函数,用于初始化结构体的成员
    struct Point
    {
        public int X;
        public int Y;
    
        public Point(int x, int y)
        {
            X = x;
            Y = y;
        }
    }
  • 结构体的实例化和访问成员与类类似。

    csharp
    Point p = new Point(10, 20); // 创建了一个 `Point` 类型的变量,并传递了初始化参数 `10` 和 `20`
    // Point point = new Point();
    // point.X = 10;
    // point.Y = 20;
    
    Console.WriteLine($"X: {p.X}, Y: {p.Y}"); // 访问了它的成员 `X` 和 `Y`
  • 结构是值类型 (类是引用类型)

  • 结构中可以定义字段、属性和方法

  • 不能为结构中的字段赋初始值

  • 结构的构造方法中必须为所有字段赋值

  • 不能为结构显式定义无参数的构造方法

  • 结构类型的对象可以不实例化

结构体和类的区别

值类型和引用类型

结构体是值类型,而类是引用类型。

  • 当您声明一个结构体变量时,实际上是在栈中为其分配内存,并将其值直接存储在该内存中。
  • 当您声明一个类变量时,只是在栈中分配一个引用,该引用指向在堆中分配的对象。
  • 因此,当您对结构体变量进行操作时,将使用其实际值,而不是引用。
  • 而对类变量进行操作时,将使用引用,而不是实际值。
csharp
public struct MyStruct
{
    public int x;
    public int y;
    public MyStruct(int x, int y)
    {
        this.x = x;
        this.y = y;
    }
}
csharp
public class MyClass
{
    public int x;
    public int y;
    public MyClass(int x, int y)
    {
        this.x = x;
        this.y = y;
    }
}

默认构造函数

  • 当您声明一个类时,C#编译器将为您自动生成一个默认构造函数,该构造函数将初始化类的所有成员变量。
  • 而在声明结构体时,如果您没有提供自己的构造函数,则 C# 编译器将为您生成一个默认构造函数,该构造函数将初始化结构体的所有字段。
csharp
struct Point {
    public int x;
    public int y;
}

public class Program
{
    static void Main(string[] args)
    {
        Point p1 = new Point();
        p1.x = 10;
        p1.y = 20;
    }
}
csharp
class Person {
    public string name;
    public int age;
}

public class Program
{
    static void Main(string[] args)
    {
        Person person1 = new Person();
        person1.name = "John";
        person1.age = 30;
    }
}

成员访问修饰符

  • 在结构体和类中,您可以使用公共,私有和受保护等访问修饰符来限制成员的可见性和可访问性。
  • 结构体的默认访问修饰符为公共。这意味着结构体的所有字段都可以从外部访问,而不管您是否将它们标记为公共。
csharp
struct Point {
    public int x;
    public int y;
}
csharp
class Person {
    private string name;
    public int age;
}

继承

类可以继承其他类,但结构体不能。这意味着您可以使用类来实现多态和基类/子类关系,但不能使用结构体。

csharp
class Animal {
    public virtual void MakeSound() {
        Console.WriteLine("The animal makes a sound");
    }
}

class Dog : Animal {
    public override void MakeSound() {
        Console.WriteLine("The dog barks");
    }
}

Dog类继承自Animal类,并覆盖了MakeSound方法以提供自己的实现。

性能

  • 由于结构体是值类型,因此它们在内存中的分配通常比类更快。这是因为它们的值存储在栈中,而不是堆中。
  • 在一些情况下,使用结构体可以提高应用程序的性能。
csharp
/// <summary>
/// 定义结构体`Point`来表示二维坐标系中的点
/// </summary>
struct Point {
    public int x;
    public int y;

    /// <summary>
    /// 计算两个点之间距离的方法`Distance`
    /// 通过将两个点作为参数传递给`Distance`方法来计算它们之间的距离
    /// </summary>
    public double Distance(Point other) {
        int xDiff = x - other.x;
        int yDiff = y - other.y;

        return Math.Sqrt(xDiff * xDiff + yDiff * yDiff);
    }
}

Point p1 = new Point() { x = 10, y = 20 };
Point p2 = new Point() { x = 30, y = 40 };
double distance = p1.Distance(p2);

定义时字段赋初始值

  • 结构体中的字段不能在定义字段时赋值,但可以在声明时或构造函数中进行赋值。

    • 结构体中的字段不能在字段定义时直接赋初值,这是因为结构体是值类型,而值类型的初始化是通过构造函数进行的。在结构体中定义字段时,编译器不会为字段生成默认的构造函数,因此在字段定义时不能为字段赋初始值。
    • 如果您想要在结构体中的字段定义时为字段赋初始值,您可以创建一个带有参数的构造函数,并在其中为字段赋值。
  • 类中的字段可以在定义时赋初始值,也可以在构造函数中赋值。

csharp
struct Person
{
    public string name;
    public int age;
    public bool isMarried;
}

class Program
{
    static void Main(string[] args)
    {
        Person person = new Person
        {
            name = "Alice",
            age = 30,
            isMarried = true
        };

        // Output: Alice, 30, True
        Console.WriteLine("{0}, {1}, {2}", person.name, person.age, person.isMarried);
    }
}
csharp
class Person
{
    public string name = "Alice";
    public int age = 30;
    public bool isMarried = true;
}

class Program
{
    static void Main(string[] args)
    {
        Person person = new Person();

        // Output: Alice, 30, True
        Console.WriteLine("{0}, {1}, {2}", person.name, person.age, person.isMarried);
    }
}

显式定义无参构造

  • 结构体中不能显式定义无参构造方法。
  • 类中可以显式定义无参构造方法。
csharp
struct Person
{
    public string name;
    public int age;
    public bool isMarried;

    public Person(string name, int age, bool isMarried)
    {
        this.name = name;
        this.age = age;
        this.isMarried = isMarried;
    }
}

class Program
{
    static void Main(string[] args)
    {
        Person person = new Person("Alice", 30, true);

        // Output: Alice, 30, True
        Console.WriteLine("{0}, {1}, {2}", person.name, person.age, person.isMarried);
    }
}
csharp
class Person
{
    public string name;
    public int age;
    public bool isMarried;

    public Person()
    {
        name = "Alice";
        age = 30;
        isMarried = true;
    }

    public Person(string name, int age, bool isMarried)
    {
        this.name = name;
        this.age = age;
        this.isMarried = isMarried;
    }
}

class Program
{
    static void Main(string[] args)
    {
        Person person = new Person();

        // Output: Alice, 30, True
        Console.WriteLine("{0}, {1}, {2}", person.name, person.age, person.isMarried);
    }
}

调用前是否需要实例化

  • 结构体类型的对象可以不实例化
  • 类类型的对象必须实例化
csharp
struct Person
{
    public string name;
    public int age;
    public bool isMarried;

    public void Print()
    {
        Console.WriteLine("{0}, {1}, {2}", name, age, isMarried);
    }
}

class Program
{
    static void Main(string[] args)
    {
        Person person = new Person();
        person.Print();
    }
}
csharp
class Person
{
    public string? name;
    public int age;
    public bool isMarried;

    public void Print()
    {
        Console.WriteLine("{0}, {1}, {2}", name, age, isMarried);
    }
}

class Program
{
    static void Main(string[] args)
    {
        Person person = new Person();
        person.name = "Alice";
        person.age = 30;
        person.isMarried = true;
        person.Print();
    }
}

有参构造必须为所有字段赋值

  • 结构体中的有参构造方法必须为所有字段赋值。
  • 类中的有参构造方法可以为所有字段赋值,也可以为部分字段赋值。
csharp
struct Person
{
    public string name;
    public int age;
    public bool isMarried;

    public Person(string name, int age, bool isMarried)
    {
        this.name = name;
        this.age = age;
        this.isMarried = isMarried;
    }
}

class Program
{
    static void Main(string[] args)
    {
        Person person = new Person("Alice", 30, true);

        // Output: Alice, 30, True
        Console.WriteLine("{0}, {1}, {2}", person.name, person.age, person.isMarried);
    }
}
csharp
class Person
{
    public string name;
    public int age;
    public bool isMarried;

    public Person(string name, int age, bool isMarried)
    {
        this.name = name;
        this.age = age;
        this.isMarried = isMarried;
    }

    public Person(string name, int age)
    {
        this.name = name;
        this.age = age;
    }
}

class Program
{
    static void Main(string[] args)
    {
        Person person = new Person("Alice", 30, true);

        // Output: Alice, 30, True
        Console.WriteLine("{0}, {1}, {2}", person.name, person.age, person.isMarried);
    }
}

示例代码

对结构体/类变量进行操作

csharp
/// <summary>
/// 在控制台上打印结构体和类的坐标值
/// </summary>
class Program
{
    static void Main(string[] args)
    {
        MyStruct structObj = new MyStruct(1, 2);
        MyClass classObj = new MyClass(3, 4);

        Console.WriteLine($"structObj.x: {structObj.x}, structObj.y: {structObj.y}");
        Console.WriteLine($"classObj.x: {classObj.x}, classObj.y: {classObj.y}");

        UpdateValues(structObj, classObj);

        Console.WriteLine($"structObj.x: {structObj.x}, structObj.y: {structObj.y}");
        Console.WriteLine($"classObj.x: {classObj.x}, classObj.y: {classObj.y}");

        Console.ReadKey();
    }

    static void UpdateValues(MyStruct structObj, MyClass classObj)
    {
        structObj.x = 5;
        structObj.y = 6;
        classObj.x = 7;
        classObj.y = 8;
    }
}

// 输出
// structObj.x: 1, structObj.y: 2
// classObj.x: 3, classObj.y: 4
// structObj.x: 1, structObj.y: 2
// classObj.x: 7, classObj.y: 8

可以看到,尽管我们在 UpdateValues 方法中更改了类对象的属性,但 Main 方法中的 classObj 对象的属性值也随之更改了。这是因为类是引用类型,传递给 UpdateValues 方法的是指向同一个对象的引用。在方法内部修改该对象的属性值,也就会影响到 Main 方法中的该对象。

而对于结构体,情况则不同。在 UpdateValues 方法中修改 structObj 的属性值并不会影响到 Main 方法中的 structObj 对象,因为结构体是值类型,传递给 UpdateValues 方法的是 structObj 的副本,而不是指向同一个对象的引用。

结构体和类的构造函数的使用

csharp
/// <summary>
/// 定义类 `StudentClass` 来表示学生
/// </summary>
public class StudentClass
{
    public string name;
    public int age;
    public StudentClass(string name, int age)
    {
        this.name = name;
        this.age = age;
    }

    /// <summary>
    /// 在控制台上打印学生姓名和年龄
    /// </summary>
    public void PrintDetails()
    {
        Console.WriteLine($"Student name: {name}, age: {age}");
    }
}
csharp
/// <summary>
/// 定义结构体 `StudentStruct` 来表示学生
/// </summary>
public struct StudentStruct
{
    public string name;
    public int age;
    public StudentStruct(string name, int age)
    {
        this.name = name;
        this.age = age;
    }

    /// <summary>
    /// 在控制台上打印学生姓名和年龄
    /// </summary>
    public void PrintDetails()
    {
        Console.WriteLine($"Student name: {name}, age: {age}");
    }
}
csharp
static void Main(string[] args)
{
    StudentStruct structObj = new StudentStruct("Tom", 20); // 创建一个学生结构体对象
    StudentClass classObj = new StudentClass("Jerry", 21); // 创建一个学生类对象

    // 调用结构体和类的 PrintDetails 方法
    structObj.PrintDetails();
    classObj.PrintDetails();

    // 等待用户输入
    Console.ReadKey();
}

// Student name: Tom, age: 20
// Student name: Jerry, age: 21

可以看到,结构体和类都可以有自己的构造函数和方法,并且它们的行为非常相似。

比较值类型和引用类型的存储位置

  • 比较值类型和引用类型的存储位置
  • 结构体和类中定义构造函数
  • 比较值类型和引用类型在传递方式上的差异
csharp
using System;

/// <summary>
/// 定义结构体 `PointStruct` 来表示坐标点
/// </summary>
public struct PointStruct
{
    public int x;
    public int y;
}

/// <summary>
/// 定义类 `PointClass` 来表示坐标点
/// </summary>
public class PointClass
{
    public int x;
    public int y;
}

/// <summary>
/// 定义结构体 `MyStruct` 来表示学生并包含一个坐标点结构体 `PointStruct`
/// </summary>
public struct MyStruct
{
    public PointStruct point;
    public string name;
    public int age;

    /// <summary>
    /// 更新结构体的属性值
    /// </summary>
    public void UpdateValues()
    {
        point.x = 10;
        point.y = 20;
        name = "New Name";
        age = 30;
    }
}

/// <summary>
/// 定义类 `MyClass` 来表示学生并包含一个坐标点类 `PointClass`
/// </summary>
public class MyClass
{
    public PointClass point;
    public string name;
    public int age;

    /// <summary>
    /// 更新类的属性值
    /// </summary>
    public void UpdateValues()
    {
        point.x = 10;
        point.y = 20;
        name = "New Name";
        age = 30;
    }
}

/// <summary>
/// 定义结构体 `StudentStruct` 来表示学生
/// </summary>
public struct StudentStruct
{
    public string name;
    public int age;
    public StudentStruct(string name, int age)
    {
        this.name = name;
        this.age = age;
    }

    /// <summary>
    /// 在控制台上打印学生姓名和年龄
    /// </summary>
    public void PrintDetails()
    {
        Console.WriteLine($"Student name: {name}, age: {age}");
    }
}

/// <summary>
/// 定义类 `StudentClass` 来表示学生
/// </summary>
public class StudentClass
{
    public string name;
    public int age;
    public StudentClass(string name, int age)
    {
        this.name = name;
        this.age = age;
    }

    /// <summary>
    /// 在控制台上打印学生姓名和年龄
    /// </summary>
    public void PrintDetails()
    {
        Console.WriteLine($"Student name: {name}, age: {age}");
    }
}

class Program
{
    static void Main(string[] args)
    {
        // 示例 1:比较值类型和引用类型的存储位置
        PointStruct structObj1 = new PointStruct();
        PointClass classObj1 = new PointClass();
        Console.WriteLine($"Struct object1: {sizeof(PointStruct)} bytes, Class object1: {sizeof(PointClass)} bytes");

        PointStruct structObj2 = new PointStruct();
        PointClass classObj2 = new PointClass();
        Console.WriteLine($"Struct object2: {sizeof(PointStruct)} bytes, Class object2: {sizeof(PointClass)} bytes");

        Console.WriteLine();

        // 示例 2:比较值类型和引用类型在传递方式上的差异
        MyStruct structObj = new MyStruct();
        MyClass classObj = new MyClass();
        structObj.point.x = 1;
        structObj.point.y = 2;
        structObj.name = "Old Name";
        structObj.age = 20;
        classObj.point = new PointClass { x = 1, y = 2 };
        classObj.name = "Old Name";
        classObj.age = 20;

        Console.WriteLine("Before UpdateValues method:");
        Console.WriteLine($"Struct point: ({structObj.point.x}, {structObj.point.y}), name: {structObj.name}, age: {structObj.age}");
        Console.WriteLine($"Class point: ({classObj.point.x}, {classObj.point.y}), name: {classObj.name}, age: {classObj.age}");

        structObj.UpdateValues();
        classObj.UpdateValues();

        Console.WriteLine("After UpdateValues method:");
        Console.WriteLine($"Struct point: ({structObj.point.x}, {structObj.point.y}), name: {structObj.name}, age: {structObj.age}");
        Console.WriteLine($"Class point: ({classObj.point.x}, {classObj.point.y}), name: {classObj.name}, age: {classObj.age}");

        Console.WriteLine();

        // 示例 3:结构体和类中定义构造函数
        StudentStruct structStudent = new StudentStruct("John", 20);
        structStudent.PrintDetails();

        StudentClass classStudent = new StudentClass("John", 20);
        classStudent.PrintDetails();
    }
}

// 输出:
// Struct object1: 8 bytes, Class object1: 8 bytes
// Struct object2: 8 bytes, Class object2: 8 bytes

// Before UpdateValues method:
// Struct point: (1, 2), name: Old Name, age: 20
// Class point: (1, 2), name: Old Name, age: 20
// After UpdateValues method:
// Struct point: (10, 20), name: New Name, age: 30
// Class point: (10, 20), name: New Name, age: 30

// Student name: John, age: 20
// Student name: John, age: 20

从输出结果可以看出:

  1. 结构体的实例分配在栈中,而类的实例分配在堆中,所以结构体的实例大小可能会比类的实例小。
  2. 结构体是值类型,类是引用类型。在传递结构体实例时,会进行复制传递;在传递类实例时,会进行引用传递。
  3. 结构体和类都可以定义构造函数,但结构体的构造函数不能有参数 less 构造函数。
  4. 当使用结构体实例调用方法时,如果方法修改结构体的字段,则实际上修改的是方法内的一个副本,而不是原始结构体实例。因此,对结构体实例进行修改的方法必须是值类型方法(即不修改实例字段的方法)或者使用 refout 关键字传递结构体实例。
  5. 类的实例是通过引用访问的,因此对类实例的修改会直接反映在原始实例上。
  6. 结构体比类更适合存储简单数据类型,例如数值类型或坐标点等。
  7. 类比结构体更适合用于创建大型对象或维护状态,例如用户界面或数据库记录。

在选择结构体或类时,应根据其特性和使用场景进行评估。如果需要存储一组简单的数据,那么结构体可能是更好的选择;如果需要创建一个复杂的对象,并且需要维护状态,那么类可能是更好的选择。

总结

总结来说,结构体和类在 C#中都是用于定义自定义数据类型的语言构造,它们在定义方式和语法上很相似,但在使用时有很多差异,包括但不限于:

  • 结构体是值类型,而类是引用类型;
  • 结构体不能被继承,而类可以被继承;
  • 结构体可以没有构造函数;
  • 在传递给方法时,结构体会被复制,而类会被引用。
  • 当定义一个结构体时,它的实例将被分配在栈中,而当定义一个类时,它的实例将被分配在堆中。
  • 在选择使用结构体或类时,需要考虑数据类型的大小、生命周期、使用方式等因素。
    • 一般来说,当需要表示一个小型数据类型时(如坐标、颜色等),使用结构体可能更合适;
    • 当需要表示一个大型数据类型时(如人、订单等),使用类可能更合适。

最后,让我们再次总结一下结构体和类的区别,同时加上上述讨论的内容:

属性结构体
存储位置
是否支持继承不支持支持
传递方式复制传递引用传递
适用场景小型数据类型大型数据类型
实例修改方式值类型方法引用类型方法(直接反映在原始实例上)
字段赋初始值不可以可以
是否需要显式定义构造函数不需要可以不定义
显式定义无参构造函数不可以可以
有参构造方法中为所有字段赋值必须可以不赋值
结构类型的对象是否需要实例化后才能调用里面的方法不需要需要