C# 结构体

文章目录


前言

结构体(struct)是一种极为重要的值类型(value type)数据结构,能够将各种相关的数据有条理地组织并存储起来,为我们处理和操作数据提供了一种高效且灵活的方式。

一、结构体的定义与基本使用

(一)定义结构体

在 C# 里,我们使用 struct 关键字来创建结构体。struct 语句的作用就像是为程序打造了一个带有多个成员的全新数据类型模板,通过它可以定义出符合特定需求的数据结构。例如,当我们想要跟踪图书馆中书的各种相关信息时,就可以定义一个名为 Books 的结构体来承载这些信息。
如下所示:

csharp 复制代码
struct Books
{
    public string title;
    public string author;
    public string subject;
    public int book_id;
};

在这个 Books 结构体的定义中,包含了表示书籍标题(title)、作者(author)、主题(subject)以及书籍编号(book_id)的成员变量,它们的类型分别为 string 和 int,通过这些成员变量,我们能够全面地描述一本书的关键属性。

(二)结构体的使用示例

以下是一个展示结构体用法的完整程序示例:

csharp 复制代码
using System;
using System.Text;

struct Books
{
    public string title;
    public string author;
    public string subject;
    public int book_id;
};

public class testStructure
{
    public static void Main(string[] args)
    {
        // 声明两个 Books 类型的结构体变量 Book1 和 Book2
        Books Book1;        /* 声明 Book1,类型为 Books */
        Books Book2;        /* 声明 Book2,类型为 Books */

        // 为 Book1 的各个成员变量赋值,详细描述一本书的信息
        Book1.title = "C Programming";
        Book1.author = "Nuha Ali";
        Book1.subject = "C Programming Tutorial";
        Book1.book_id = 6495407;

        // 同样地,为 Book2 赋值,描述另一本书的情况
        Book2.title = "Telecom Billing";
        Book2.author = "Zara Ali";
        Book2.subject = "Telecom Billing Tutorial";
        Book2.book_id = 6495700;

        // 打印 Book1 的详细信息
        Console.WriteLine("Book 1 title : {0}", Book1.title);
        Console.WriteLine("Book 1 author : {0}", Book1.author);
        Console.WriteLine("Book 1 subject : {0}", Book1.subject);
        Console.WriteLine("Book 1 book_id :{0}", Book1.book_id);

        // 打印 Book2 的详细信息
        Console.WriteLine("Book 2 title : {0}", Book2.title);
        Console.WriteLine("Book 2 author : {0}", Book2.author);
        Console.WriteLine("Book 2 subject : {0}", Book2.subject);
        Console.WriteLine("Book 2 book_id : {0}", Book2.book_id);

        Console.ReadKey();
    }
}

当上述代码被编译并执行后,会输出如下结果:

plaintext 复制代码
Book 1 title : C Programming
Book 1 author : Nuha Ali
Book 1 subject : C Programming Tutorial
Book 1 book_id : 6495407
Book 2 title : Telecom Billing
Book 2 author : Zara Ali
Book 2 subject : Telecom Billing Tutorial
Book 2 book_id : 6495700

从这个示例可以清晰地看到,我们通过结构体变量能够方便地访问和操作其内部的各个成员变量,分别对不同的书籍信息进行赋值和展示,就像使用自定义的数据类型一样自然流畅,这充分体现了结构体在组织和管理相关数据方面的便利性。

二、C# 结构的特点

(一)丰富的成员类型

结构体并非仅仅局限于存储简单的数据,它还具备很强的扩展性,可以带有方法、字段、索引、属性、运算符方法和事件等多种成员类型。这使得结构体能够适用于表示各种各样的轻量级数据情况,例如坐标(可以通过包含 x 和 y 坐标值的结构体,并搭配相应的方法来进行坐标运算等操作)、范围(定义包含起始值和结束值的结构体,以及判断是否包含某个值的方法等)、日期、时间等。

(二)构造函数相关限制与特性

结构体可以定义构造函数,不过需要注意的是,它不能定义析构函数。而且,结构体不能定义无参构造函数,这一点与类有着明显的区别。在结构体中,无参构造函数(默认)是由系统自动定义的,并且这个默认的无参构造函数不能被我们手动改变。
例如:

csharp 复制代码
struct Point
{
    public int X;
    public int Y;
    // 合法的有参构造函数
    public Point(int x, int y)
    {
        X = x;
        Y = y;
    }
    // 以下是非法的无参构造函数定义,结构体不允许这样写
    // public Point()
    // {
    // }
}

(三)继承方面的限制

与类不同的是,结构体不能继承其他的结构或类,也不能作为其他结构或类的基础结构。这意味着结构体在继承体系方面相对独立,它更侧重于简单地封装和处理自身内部定义的数据和相关逻辑,而不像类那样可以通过继承来扩展功能、实现多态等复杂的面向对象设计模式。

(四)接口实现能力

虽然结构体不能参与继承关系,但它具备实现一个或多个接口的能力。通过实现接口,结构体可以遵循接口中定义的契约,提供特定的方法实现,从而在一定程度上增强了结构体与其他代码模块之间的交互性和通用性,使其能够更好地融入到面向对象的编程框架中。例如:

csharp 复制代码
interface IPrintable
{
    void Print();
}

struct Document : IPrintable
{
    public string Content;
    public void Print()
    {
        Console.WriteLine(Content);
    }
}

在这个示例中,Document 结构体实现了 IPrintable 接口,并重写了 Print 方法,这样就可以按照接口定义的规范来输出结构体中存储的文档内容,实现了特定的功能需求。

(五)成员修饰符限制

结构体成员不能指定为 abstract、virtual 或 protected。这是因为结构体本身的设计初衷是用于表示简单、轻量级的数据结构,避免了像类那样复杂的多态和继承相关的特性,所以这些与类的高级面向对象特性相关的修饰符在结构体中是不适用的。

(六)实例化与初始化特点

当我们使用 New 操作符创建一个结构对象时,会调用适当的构造函数来创建结构。不过,结构体有一个很独特的地方,那就是它可以不使用 New 操作符即可被实例化。但如果不使用 New 操作符,只有在所有的字段都被初始化之后,字段才被赋值,对象才能够被正常使用。例如:

csharp 复制代码
struct SimpleStruct
{
    public int Value;
}

class Program
{
    static void Main()
    {
        SimpleStruct struct1;
        // 以下这种不使用 New 操作符的方式,需要先初始化字段
        struct1.Value = 10;
        // 此时 struct1 才能正常使用,比如进行后续操作

        SimpleStruct struct2 = new SimpleStruct();
        // 使用 New 操作符创建结构体对象,字段会按照构造函数或默认规则初始化
    }
}

(七)内存分配与性能特点

结构体变量通常分配在栈上,这是结构体在内存管理方面的一个重要特性。由于栈内存的分配和释放速度相对较快,所以结构体在创建和销毁时往往比类更加高效,使得它们在性能上具有一定的优势,尤其适用于那些需要频繁创建和销毁的简单数据对象场景。然而,如果将结构用作类的字段,且这个类是引用类型,那么此时结构将存储在堆上,这是因为类的对象本身存储在堆上,其内部包含的结构体字段也会随之存储在堆中。

(八)可变性特点

结构体默认情况下是可变的,这意味着我们可以直接修改它们的字段值。
例如:

csharp 复制代码
struct MutableStruct
{
    public int Data;
}

class Program
{
    static void Main()
    {
        MutableStruct struct1 = new MutableStruct();
        struct1.Data = 5;
        // 可以成功修改结构体的字段值
    }
}

但如果我们将结构定义为只读(通过将结构体的成员设置为只读属性等方式来实现),那么它的字段将是不可变的,这样可以保证结构体在使用过程中的数据稳定性,避免意外的修改。

三、类 vs 结构

在 C# 编程中,类和结构虽然都用于构建对象、组织数据和实现功能,但它们在设计和使用上有着诸多不同的考虑因素,各自适用于不同的编程场景。

(一)值类型 vs 引用类型

结构体是值类型(Value Type):结构体作为值类型,其内存分配是在栈上进行的(特殊情况下如作为类的字段会存储在堆上)。当我们将结构实例传递给方法或者赋值给另一个变量时,会复制整个结构的内容,这就相当于创建了一个全新的、独立的副本。
例如:

csharp 复制代码
struct MyStruct
{
    public int X;
    public int Y;
}

class Program
{
    static void Main()
    {
        MyStruct structInstance1 = new MyStruct { X = 1, Y = 2 };
        MyStruct structInstance2 = structInstance1;
        structInstance1.X = 5;
        Console.WriteLine($"Struct: {structInstance1.X}, {structInstance2.X}");
    }
}

在上述代码中,修改 structInstance1 的 X 值并不会影响 structInstance2 的 X 值,因为它们是两个独立的副本。

类是引用类型(Reference Type):类作为引用类型,其内存分配是在堆上完成的。当把类实例传递给方法或者赋值给另一个变量时,实际上传递的是引用(也就是内存地址),而并非整个对象的副本。这意味着多个变量可能指向同一个对象,对其中一个变量所做的修改会影响到其他指向该对象的变量。
例如:

csharp 复制代码
class MyClass
{
    public int X;
    public int Y;
}

class Program
{
    static void Main()
    {
        MyClass classInstance1 = new MyClass { X = 3, Y = 4 };
        MyClass classInstance2 = classInstance1;
        classInstance1.X = 6;
        Console.WriteLine($"Class: {classInstance1.X}, {classInstance2.X}");
    }
}

在这个例子中,修改 classInstance1 的 X 值后,classInstance2 的 X 值也会随之改变,因为它们都指向堆上的同一个对象。

(二)继承和多态性

  • 结构不能继承: 结构体无法继承其他结构或类,也不能作为其他结构或类的基类,它在继承体系方面相对封闭,主要聚焦于自身内部的数据封装和简单操作逻辑。
    例如,以下代码是非法的:
csharp 复制代码
struct MyStruct
{
    // 结构不能继承,以下代码会报错
    // struct MyDerivedStruct : MyBaseStruct
    // {
    // }
}
  • 类支持继承: 类则具备强大的继承和多态性支持,我们可以通过派生新类来扩展现有类的功能,实现代码的复用和面向对象设计中的多态效果。
    例如:
csharp 复制代码
class MyBaseClass
{
    public virtual void DoSomething()
    {
        Console.WriteLine("Base class method");
    }
}

class MyDerivedClass : MyBaseClass
{
    public override void DoSomething()
    {
        Console.WriteLine("Derived class method");
    }
}

class Program
{
    static void Main()
    {
        MyBaseClass instance = new MyDerivedClass();
        instance.DoSomething();
    }
}

在上述代码中,MyDerivedClass 继承自 MyBaseClass,并重写了 DoSomething 方法,通过多态机制,根据对象的实际类型来决定调用哪个类的 DoSomething 方法,展示了类在继承和多态方面的强大功能。

(三)默认构造函数

  • 结构不能有无参数的构造函数: 结构体不允许包含无参数的构造函数,只能定义有参数的构造函数来对结构体的成员进行初始化。例如:
csharp 复制代码
struct MyStruct
{
    public int X;
    public int Y;
    // 以下是非法的无参构造函数定义,结构体不允许这样写
    // public MyStruct()
    // {
    // }
    public MyStruct(int x, int y)
    {
        X = x;
        Y = y;
    }
}
  • 类可以有无参数的构造函数: 类不仅可以包含有参数的构造函数,还可以有无参数的构造函数。如果在类的定义中没有提供任何构造函数,系统会自动提供一个默认的无参数构造函数,方便类的实例化。例如:
csharp 复制代码
class MyClass
{
    public int X;
    public int Y;
    // 合法的无参数构造函数定义
    public MyClass()
    {
    }
    public MyClass(int x, int y)
    {
        X = x;
        Y = y;
    }
}

(四)赋值行为

  • 类型为类的变量在赋值时存储的是引用: 当对类类型的变量进行赋值操作时,实际上只是复制了对象的引用,所以两个变量会指向同一个对象。例如:
csharp 复制代码
class MyClass
{
    public int Value;
}

class Program
{
    static void Main()
    {
        MyClass obj1 = new MyClass { Value = 10 };
        MyClass obj2 = obj1;
        obj2.Value = 20;
        Console.WriteLine($"obj1.Value: {obj1.Value}");
    }
}

在上述代码中,修改 obj2 的 Value 值后,obj1 的 Value 值也会变为 20,因为它们指向同一个对象。

结构变量在赋值时会复制整个结构:而对于结构体变量,赋值操作会复制整个结构的内容,每个变量都拥有自己独立的副本,相互之间不会产生影响。例如:

csharp 复制代码
struct MyStruct
{
    public int Value;
}

class Program
{
    static void Main()
{
        MyStruct struct1 = new MyStruct { Value = 10 };
        MyStruct struct2 = struct1;
        struct1.Value = 20;
        Console.WriteLine($"struct1.Value: {struct1.Value}, struct2.Value: {struct2.Value}");
    }
}

在这个例子中,修改 struct1 的 Value 值并不会改变 struct2 的 Value 值,因为它们是各自独立的结构副本。

(五)传递方式

  • 类型为类的对象在方法调用时通过引用传递: 在方法调用过程中,传递类类型的对象时,传递的是对象的引用,这就意味着在方法内部对该对象所做的任何更改都会影响到原始对象。例如:
csharp 复制代码
class MyClass
{
    public int Value;
}

class Program
{
    static void ModifyClassObject(MyClass obj)
    {
        obj.Value = 30;
    }

    static void Main()
    {
        MyClass myObj = new MyClass { Value = 20 };
        ModifyClassObject(myObj);
        Console.WriteLine($"myObj.Value: {myObj.Value}");
    }
}

在上述代码中,在 ModifyClassObject 方法中修改了传入对象的 Value 值后,回到 Main 方法中,原始的 myObj 对象的 Value 值也已经被改变了。

结构对象通常通过值传递:而对于结构体对象,在方法调用时传递的是结构的副本,并非原始的结构对象本身,所以在方法中对结构所做的更改不会影响到原始对象。例如:

csharp 复制代码
struct MyStruct
{
    public int Value;
}

class Program
{
    static void ModifyStructObject(MyStruct structObj)
    {
        structObj.Value = 40;
    }

    static void Main()
    {
        MyStruct myStruct = new MyStruct { Value = 30 };
        ModifyStructObject(myStruct);
        Console.WriteLine($"myStruct.Value: {myStruct.Value}");
    }
}

在这个例子中,尽管在 ModifyStructObject 方法中修改了传入的结构体对象的 Value 值,但回到 Main 方法中,原始的 myStruct 对象的 Value 值并没有改变,因为传递的只是副本。

(六)可空性

  • 结构体是值类型,不能直接设置为 null: 由于 null 是引用类型的默认值,而不是值类型的默认值,所以结构体本身不能直接被赋值为 null。但如果我们需要表示结构体变量的缺失或无效状态,可以借助 Nullable(也可写成 T?)这种可空类型来实现。例如,假设有一个表示学生成绩的结构体,有时候成绩可能还未录入(即无效状态),就可以这样定义:
csharp 复制代码
struct StudentScore
{
    public int? MathScore;
    public int? EnglishScore;
}

class Program
{
    static void Main()
    {
        StudentScore score = new StudentScore();
        score.MathScore = null;
        score.EnglishScore = 80;

        if (score.MathScore.HasValue)
        {
            Console.WriteLine($"数学成绩为: {score.MathScore.Value}");
        }
        else
        {
            Console.WriteLine("数学成绩尚未录入");
        }

        Console.WriteLine($"英语成绩为: {score.EnglishScore}");
    }
}

在上述代码中,通过使用 int? 类型来定义结构体中的成绩成员变量,就能灵活地表示成绩存在或缺失的不同状态了。

类默认可为 null:类作为引用类型,其实例默认是可以为 null 的。这意味着在声明一个类类型的变量时,如果没有对其进行实例化赋值,它的值就是 null,表示该变量目前没有指向任何实际的对象。例如:

csharp 复制代码
class Person
{
    public string Name;
}

class Program
{
    static void Main()
    {
        Person person;
        if (person == null)
        {
            Console.WriteLine("person 变量目前未指向任何对象");
        }

        person = new Person { Name = "John" };
        Console.WriteLine($"这个人的名字是: {person.Name}");
    }
}

在这个例子中,一开始声明 person 变量时它的值为 null,直到后续通过 new 操作符创建了 Person 类的实例并赋值给它,才使其指向了一个实际的对象。

(七)性能和内存分配

  • 结构通常更轻量: 由于结构体是值类型且大多在栈上分配内存,相比于类,它们通常更加轻量级。在处理一些简单的数据表示场景时,结构体能够减少内存开销,并且在创建和销毁对象时,因为栈内存操作的高效性,速度也更快。例如,在一个游戏中表示角色的坐标信息,如果使用结构体来存储 (x, y) 坐标,大量创建和更新这些坐标结构体对象时,相较于使用类来存储,在性能和内存占用方面往往更有优势。
  • 类可能有更多开销: 类作为引用类型,存储在堆上,其内存管理相对复杂一些,可能涉及到更多的内存开销,比如对象的引用计数、垃圾回收等机制都会带来一定的性能损耗。而且在创建类对象时,需要在堆上分配内存空间,这个过程相对栈上分配会更耗时一些。不过,类的这种设计也使得它能够处理更复杂的对象关系、实现继承和多态等高级功能,适用于构建大型、复杂的软件系统中的各种对象模型。
      以下通过一个综合示例再次对比类和结构在上述各方面的不同表现:
csharp 复制代码
using System;

// 结构声明
struct MyStruct
{
    public int X;
    public int Y;

    // 有参数的构造函数
    public MyStruct(int x, int y)
    {
        X = x;
        Y = y;
    }
}

// 类声明
class MyClass
{
    public int X;
    public int Y;

    // 类可以有无参数的构造函数
    public MyClass()
    {
    }

    // 有参数的构造函数
    public MyClass(int x, int y)
    {
        X = x;
        Y = y;
    }
}

class Program
{
    static void Main()
    {
        // 结构是值类型,分配在栈上
        MyStruct structInstance1 = new MyStruct(1, 2);
        MyStruct structInstance2 = structInstance1; // 复制整个结构

        // 类是引用类型,分配在堆上
        MyClass classInstance1 = new MyClass(3, 4);
        MyClass classInstance2 = classInstance1; // 复制引用,指向同一个对象

        // 修改结构实例不影响其他实例
        structInstance1.X = 5;
        Console.WriteLine($"Struct: {structInstance1.X}, {structInstance2.X}");

        // 修改类实例会影响其他实例
        classInstance1.X = 6;
        Console.WriteLine($"Class: {classInstance1.X}, {classInstance2.X}");

        // 演示结构体不能有无参数构造函数
        // MyStruct structNoParam = new MyStruct(); // 编译会报错

        // 演示类可以有无参数构造函数
        MyClass classNoParam = new MyClass();

        // 演示结构体不能继承
        // struct MyDerivedStruct : MyStruct // 编译会报错
        // {
        // }

        // 演示类支持继承
        class MyDerivedClass : MyClass
        {
        }

        // 演示结构体变量赋值是复制整个结构
        MyStruct structCopy = new MyStruct(7, 8);
        MyStruct structCopy2 = structCopy;
        structCopy.X = 9;
        Console.WriteLine($"Struct Copy: {structCopy.X}, {structCopy2.X}");

        // 演示类变量赋值是复制引用
        MyClass classCopy = new MyClass(10, 11);
        MyClass classCopy2 = classCopy;
        classCopy.X = 12;
        Console.WriteLine($"Class Copy: {classCopy.X}, {classCopy2.X}");

        // 演示结构体在方法调用时通过值传递,方法内修改不影响原始对象
        MyStruct structForMethod = new MyStruct(13, 14);
        ModifyStruct(structForMethod);
        Console.WriteLine($"Struct in Method: {structForMethod.X}");

        // 演示类在方法调用时通过引用传递,方法内修改影响原始对象
        MyClass classForMethod = new MyClass(15, 16);
        ModifyClass(classForMethod);
        Console.WriteLine($"Class in Method: {classForMethod.X}");
    }

    static void ModifyStruct(MyStruct structObj)
    {
        structObj.X = 17;
    }

    static void ModifyClass(MyClass classObj)
    {
        classObj.X = 18;
    }
}
相关推荐
大耳猫6 分钟前
Android 基于Camera2 API进行摄像机图像预览
android·kotlin·相机·camera
MYBOYER9 分钟前
Kotlin DSL Gradle 指南
android·开发语言·kotlin
m0_6754470810 分钟前
Solon 拉取 maven 包很慢或拉不了,怎么办?
java·maven
武昌库里写JAVA14 分钟前
SpringCloud+SpringCloudAlibaba学习笔记
java·开发语言·算法·spring·log4j
爱编程的小生15 分钟前
SpringBoot Task
java·spring boot·后端
CoderJia程序员甲22 分钟前
重学SpringBoot3-异步编程完全指南
java·spring boot·后端·异步编程
小咖拉眯25 分钟前
第十六届蓝桥杯模拟赛第二期题解—Java
java·数据结构·算法·蓝桥杯·图搜索算法
扬子鳄00826 分钟前
Spring Boot自动配置机制
java·数据库·spring boot
岁岁岁平安28 分钟前
springboot实战(19)(条件分页查询、PageHelper、MYBATIS动态SQL、mapper映射配置文件、自定义类封装分页查询数据集)
java·spring boot·后端·mybatis·动态sql·pagehelper·条件分页查询
csdn_aspnet31 分钟前
C# 程序来计算三角形的面积(Program to find area of a triangle)
算法·c#