泛型Generic

基本定义

泛型无处不在,是和接口一样重要的存在。

  • 为什么需要泛型:避免成员膨胀或者类型膨胀。
  • 正交性:泛型类型(类、接口、委托…)、泛型成员(属性、方法、字段…)
  • 类型方法的参数推断
  • 泛型与委托、lambda表达式

image.png

泛化和具体化是相对的,所有泛型编程实体都不能直接拿来使用的,必须要经过特化之后才能拿来使用。

泛型类示例

示例:小商店店主,一开始只卖苹果,卖苹果会赠送盒子,用来包装苹果。如下:

  1. using System;
  2. namespace HelloGeneric
  3. {
  4. class Program
  5. {
  6. static void Main(string[] args)
  7. {
  8. Apple apple = new Apple() { Color = "Red" };
  9. Box box = new Box() { Cargo = apple };
  10. Console.WriteLine(box.Cargo.Color);
  11. }
  12. }
  13. class Apple
  14. {
  15. public string Color { get; set; }
  16. }
  17. class Box
  18. {
  19. public Apple Cargo { get; set; }
  20. }
  21. }

随着商店经营越来越好,卖的东西也越来越多的,商店开始卖书了。卖书会赠送盒子,用来包装书本。此时就面临一个问题,因为苹果已经有一个Box类,书也要一个盒子,就需另外添加一个BookBox类。如下:

  1. using System;
  2. namespace HelloGeneric
  3. {
  4. class Program
  5. {
  6. static void Main(string[] args)
  7. {
  8. Apple apple = new Apple() { Color = "Red" };
  9. AppleBox box = new AppleBox() { Cargo = apple };
  10. Console.WriteLine(box.Cargo.Color);
  11. Book book = new Book() { Name="New Book"};
  12. BookBox bookBox = new BookBox() { Cargo = book };
  13. Console.WriteLine(bookBox.Cargo.Name);
  14. }
  15. }
  16. class Apple
  17. {
  18. public string Color { get; set; }
  19. }
  20. class Book
  21. {
  22. public string Name { get; set; }
  23. }
  24. class AppleBox
  25. {
  26. public Apple Cargo { get; set; }
  27. }
  28. class BookBox
  29. {
  30. public Book Cargo { get; set; }
  31. }
  32. }

此时代码已经出现类型膨胀的问题了,因为随着商店产品越来越多,需要准备的盒子就越来越多,这样就造成box类极度膨胀,使得代码非常不好维护。对代码进行改写,只有一种盒子的方案,如下:

  1. using System;
  2. namespace HelloGeneric
  3. {
  4. class Program
  5. {
  6. static void Main(string[] args)
  7. {
  8. Apple apple = new Apple() { Color = "Red" };
  9. Book book = new Book() { Name="New Book"};
  10. Box box1 = new Box() { Apple = apple };
  11. Box box2 = new Box() { Book = book };
  12. Console.WriteLine(box1.Apple.Color);
  13. Console.WriteLine(box2.Book.Name);
  14. }
  15. }
  16. class Apple
  17. {
  18. public string Color { get; set; }
  19. }
  20. class Book
  21. {
  22. public string Name { get; set; }
  23. }
  24. class Box
  25. {
  26. public Apple Apple { get; set; }
  27. public Book Book { get; set; }
  28. }
  29. }

这时又出现了另外一个叫成员膨胀的问题,如果商店有1000种商品,那么在Box中就需要1000个对应的属性,而且每次这1000个属性中,只有一个会被使用,其余都用不着,而且每次增添新商品时,都需要修改Box类,增加新的属性,反之,如果商店不卖某个商品,还要在Box类中删除对应的商品属性。如果忘记向Box类中添加或者删除属性,那么代码都会存在BUG。进行另外一个方式的改进,在Box类中申明属性时,不要指定具体类型,使用Object类型属性,因为dotnet是单根的类型系统。如下:

  1. using System;
  2. namespace HelloGeneric
  3. {
  4. class Program
  5. {
  6. static void Main(string[] args)
  7. {
  8. Apple apple = new Apple() { Color = "Red" };
  9. Book book = new Book() { Name="New Book"};
  10. Box box1 = new Box() { Cargo = apple };
  11. Box box2 = new Box() { Cargo = book };
  12. Console.WriteLine((box1.Cargo as Apple)?.Color);
  13. }
  14. }
  15. class Apple
  16. {
  17. public string Color { get; set; }
  18. }
  19. class Book
  20. {
  21. public string Name { get; set; }
  22. }
  23. class Box
  24. {
  25. public Object Cargo { get; set; }
  26. }
  27. }

这种方法不能直接用”.”操作符调用盒子里面物品的属性,即”box1.Cargo.”后面是不会显示Color属性,需要对box1.Cargo进行强制类型转换或者使用as,即”box1.Cargo as Apple)?.Color”,这样显得十分复杂。使用泛型,会解决当前的调用复杂问题。把Box普通类改成泛型类,变成Box,TCargo是类型参数,是一个标识符,是一个泛化的类型。在使用时必须把泛型Box类进行特化,如下:

  1. using System;
  2. namespace HelloGeneric
  3. {
  4. class Program
  5. {
  6. static void Main(string[] args)
  7. {
  8. Apple apple = new Apple() { Color = "Red" };
  9. Book book = new Book() { Name="New Book"};
  10. Box<Apple> box1 = new Box<Apple>() { Cargo = apple };
  11. Box<Book> box2 = new Box<Book>() { Cargo = book };
  12. Console.WriteLine(box1.Cargo.Color);
  13. Console.WriteLine(box2.Cargo.Name);
  14. }
  15. }
  16. class Apple
  17. {
  18. public string Color { get; set; }
  19. }
  20. class Book
  21. {
  22. public string Name { get; set; }
  23. }
  24. class Box<TCargo>
  25. {
  26. public TCargo Cargo { get; set; }
  27. }
  28. }

泛型接口示例

以上示例完美解决了类型膨胀和成员膨胀的问题,再次举一个泛型接口的示例,如下:

  1. using System;
  2. namespace HelloGeneric
  3. {
  4. class Program
  5. {
  6. static void Main(string[] args)
  7. {
  8. Student<int> stu = new Student<int>();
  9. stu.Id = 101;
  10. stu.Name = "Tim";
  11. //随着学校越来越大,int类型不够装所有学生的ID,改用无符号的长整型作为学生的ID
  12. Student<ulong> stu2 = new Student<ulong>();
  13. stu2.Id = 100000000000000001;
  14. stu2.Name = "Jerry";
  15. }
  16. }
  17. /// <summary>
  18. /// 泛型IUnique接口
  19. /// </summary>
  20. /// <typeparam name="T"></typeparam>
  21. interface IUnique<T>
  22. {
  23. T Id { get; set; }//Id为成员属性
  24. }
  25. /// <summary>
  26. /// 泛型Student类,在实现泛型IUnique接口时,Student类也必须变为泛型类
  27. /// </summary>
  28. /// <typeparam name="T"></typeparam>
  29. class Student<T> : IUnique<T>
  30. {
  31. public T Id { get; set; }
  32. public string Name { get; set; }
  33. }
  34. }

以上在申明Student时,实现了IUnique接口,Student类也就成为了泛型类,还有一种实现泛型接口的方法为,当Student类在实现泛型IUnique接口时,实现的是一个特化之后的泛型接口,此时类就不再是泛型类了,如下:

  1. using System;
  2. namespace HelloGeneric
  3. {
  4. class Program
  5. {
  6. static void Main(string[] args)
  7. {
  8. Student stu = new Student();
  9. stu.Id = 100000000000000001;
  10. stu.Name = "Jerry";
  11. }
  12. }
  13. /// <summary>
  14. /// 泛型IUnique接口
  15. /// </summary>
  16. /// <typeparam name="T"></typeparam>
  17. interface IUnique<T>
  18. {
  19. T Id { get; set; }//Id为成员属性
  20. }
  21. /// <summary>
  22. /// Student类,实现特化后的泛型IUnique接口
  23. /// </summary>
  24. /// <typeparam name="T"></typeparam>
  25. class Student : IUnique<ulong>
  26. {
  27. public ulong Id { get ; set ; }
  28. public string Name { get; set; }
  29. }
  30. }

泛型集合示例

泛型之所以无处不在,在编程影响深刻,是因为有两个原因:

  • 泛型具有良好的正交性
  • 在dotnet framework中几乎所有常用的数据结构都是泛型的

编程一定是在处理数据的,大量的数据都是存储在各种各样的集合当中的,常用的集合有:数组,列表,链表,字典等,这些集合都是泛型的,他们的基接口和基类也是泛型的。

  1. using System;
  2. using System.Collections.Generic;
  3. namespace HelloGeneric
  4. {
  5. class Program
  6. {
  7. static void Main(string[] args)
  8. {
  9. IList<int> list = new List<int>();
  10. //IList<int>为带有一个类型参数的泛型接口,List<int>为带有一个类型参数的泛型类,其背后维护着一个数组,由于数组的长度是不可以改变的,而List的长度是可以改变的,所以List也叫动态数组。
  11. for (int i = 0; i < 100; i++)
  12. {
  13. list.Add(i);
  14. }
  15. foreach (var item in list)
  16. {
  17. Console.WriteLine(item);
  18. }
  19. }
  20. }
  21. }

带有多个类型参数的泛型接口和泛型类示例

很多泛型类型都带有不止一个类型参数,如IDictionary泛型接口和Dictionary泛型类都有两个类型参数。如下:

using System;
using System.Collections.Generic;

namespace HelloGeneric
{
    class Program
    {
        static void Main(string[] args)
        {
            IDictionary<int, string> dict = new Dictionary<int, string>();
            dict[1] = "Tim";
            dict[2] = "Tom";
            Console.WriteLine($"Student #1 is {dict[1]}");
            Console.WriteLine($"Student #2 is {dict[2]}");
        }
    }
}

泛型方法示例

示例:有两个整型数组,将他们像拉拉链一样组合在一起,方法的名称叫做Zip(),如下:

using System;
using System.Collections.Generic;

namespace HelloGeneric
{
    class Program
    {
        static void Main(string[] args)
        {
            int[] a1 = { 1,2,3,4,5};
            int[] a2 = { 1,2,3,4,5,6};
            double[] a3 = { 1.1,2.2,3.3,4.4,5.5};
            double[] a4 = { 1.1,2.2,3.3,4.4,5.5,6.6};
            var result = Zip(a1,a2);
            Console.WriteLine(string.Join(",",result));
        }

        static int[] Zip(int[] a, int[] b)
        {
            int[] zipped = new int[a.Length + b.Length];
            int ai = 0, bi = 0, zi = 0;
            do
            {
                if (ai<a.Length)
                {
                    zipped[zi++] = a[ai++];
                }
                if (bi < b.Length)
                {
                    zipped[zi++] = b[bi++];
                }
            } while (ai<a.Length||bi<b.Length);
            return zipped;
        }
    }
}

此时Zip方法无法运行double类型的参数,即Zip(a3,a4)编译会报错。想要运行通过:
第一个办法为将Zip方法重载,形参和返回值类型都为double类型, 但是这样又会出现成员膨胀的问题,这样的方法成员膨胀,会比属性成员膨胀更危险。因为在这两个方法中有绝大部分逻辑是重复的,如果后续要修改或升级其中方法的逻辑,那么另一个方法的逻辑也势必会需要修改或升级,一旦有一个重载的版本忘记修改或升级,那么程序就会存在隐藏的BUG。未来如果要处理float数组、long数组等更多类型的数组,那么就要不断地进行copy parse,显然这样是不行的。
第二个办法就是使用泛型方法,如下:

using System;
using System.Collections.Generic;

namespace HelloGeneric
{
    class Program
    {
        static void Main(string[] args)
        {
            int[] a1 = { 1,2,3,4,5};
            int[] a2 = { 1,2,3,4,5,6};
            double[] a3 = { 1.1,2.2,3.3,4.4,5.5};
            double[] a4 = { 1.1,2.2,3.3,4.4,5.5,6.6};
            var result = Zip(a1,a2);
            var result2 = Zip(a3,a4);
            Console.WriteLine(string.Join(",",result));
            Console.WriteLine(string.Join(",", result2));
        }
        /// <summary>
        /// Zip<T>()为泛型方法,
        /// </summary>
        /// <typeparam name="T">T为类型参数</typeparam>
        /// <param name="a">T类型的数组形参a</param>
        /// <param name="b">T类型的数组形参b</param>
        /// <returns>返回T类型的数组zipped</returns>
        static T[] Zip<T>(T[] a, T[] b)
        {
            T[] zipped = new T[a.Length + b.Length];
            int ai = 0, bi = 0, zi = 0;
            do
            {
                if (ai<a.Length)
                {
                    zipped[zi++] = a[ai++];
                }
                if (bi < b.Length)
                {
                    zipped[zi++] = b[bi++];
                }
            } while (ai<a.Length||bi<b.Length);
            return zipped;
        }
    }
}

泛型委托示例

示例:Action<>泛型委托,只能引用没有返回值的方法,如下:

using System;

namespace HelloGeneric
{
    class Program
    {
        static void Main(string[] args)
        {
            Action<string> a1 = Say;
            a1.Invoke("Tim");

            Action<int> a2 = Mul;
            a2(1);
        }

        static void Say(string str)
        {
            Console.WriteLine($"Hello,{str}!");
        }

        static void Mul(int x)
        {
            Console.WriteLine(x*100);
        }
    }
}
<br />示例:Func<>泛型委托,引用有返回值的方法,如下:
using System;

namespace HelloGeneric
{
    class Program
    {
        static void Main(string[] args)
        {
            Func<int,int,int> func1 = Add;
            Console.WriteLine(func1.Invoke(100, 200)) ;

            Func<double, double, double> func2 = Add;
            Console.WriteLine(func2(100.1,200.2));
        }

        static int Add(int a,int b)
        {
            return a + b;
        }

        static double Add(double a,double b)
        {
            return a + b;
        }
    }
}

示例:泛型委托和Lambda表达式的结合使用。

using System;

namespace HelloGeneric
{
    class Program
    {
        static void Main(string[] args)
        {
            Func<int,int,int> func1 = (a, b)=> { return a + b; };
            Console.WriteLine(func1.Invoke(100, 200)) ;

            Func<double, double, double> func2 = (a, b)=> { return a + b; };
            Console.WriteLine(func2(100.1,200.2));
        }
    }
}

partial类

基本定义

C#编译器允许我们把一个类的代码分成两部分或者多部分来编写,每一个部分都有自己的逻辑单元,而且每个部分都能以自己的进度进行版本更新,最终每个部分合起来还是同一个类,还有附带的好处就是类名也是相同的。

  • 为何需要partial类:因为它可以减少类的派生,减少派生的复杂性
  • partial类与Entity Framework
  • partial类与Windows Forms,WPF,ASP.NET Core

使用Entity Framework访问数据库示例

示例:使用Entity Framework访问数据库。首先管理NuGet程序包,添加EntityFramework框架。然后在SQL Server中添加Bookstore表,如下:
image.png
image.png
然后在当前程序集(Bookstore.Client)下新建ADO.NET实体数据模型项目,并新建连接,连接属性设置如下:
image.png
并选择Book作为模型中包括的数据库对象。如下:
image.png
此时就得到了一个映射好的Book类,如下:
image.png
测试并连接数据库,打印每本书的名字,如下:

using System;

namespace Bookstore.Client
{
    class Program
    {
        static void Main(string[] args)
        {
            var dbContext = new BookstoreEntities();
            var books = dbContext.Books;
            foreach (var book in books)
            {
                Console.WriteLine(book.Name);
            }
        }
    }
}

此时对Books右键转到定义,对Book右键转到定义时,可以看到Book类的所有属性,如下:

namespace Bookstore.Client
{
    using System;
    using System.Collections.Generic;

    public partial class Book
    {
        public int ID { get; set; }
        public string Name { get; set; }
        public Nullable<double> Price { get; set; }
    }
}

现在有一个新需求,就是让Book类能够自动报告自己的状态,即增加一个Report()方法,能报告ID,Name以及price。如果在EntityFramework框架自动生成的Book类中增加Report()方法的话,且如果数据库有改动,刷新BookstoreModel时,此类会重新自动生成,所添加的Report()方法会被自动丢弃。
解决方法:由于Book类是一个partial类,此时应该新建一个类文件(类名BookPart2),将Book类的另一部分写在这个类文件中,需注意新写的BookPart2类文件,名称空间、类名应与之前的Book类一致。如下:

namespace Bookstore.Client
{
    using System;
    using System.Collections.Generic;

    public partial class Book
    {
        public string Report() 
        {
            return $"#{this.ID} Name:{this.Name} Price:{this.Price}";
        }
    }
}

这样主程序Main就也能使用book的Report()方法了: Console.WriteLine(book.Report());
如果在SQL Server中的Book表中,新增加一列,保存后,执行编辑前200行,回到VS的BookstoreModel中右键从数据库更新模型,并保存当前代码,这时回到Book类中可以看到Book类多了一个Author属性。继续运行程序时,Report()方法一样可以被调用。

WinForm中partial类示例

新建winform程序,Form1窗体类其实就是一个partial类,他被分成了两部分,一部分为UI设计部分,一部分为后台逻辑部分,两部分都是由C#语言编写,如下:

namespace WindowsFormsApp1
{
    partial class Form1
    {
        /// <summary>
        /// 必需的设计器变量。
        /// </summary>
        private System.ComponentModel.IContainer components = null;

        /// <summary>
        /// 清理所有正在使用的资源。
        /// </summary>
        /// <param name="disposing">如果应释放托管资源,为 true;否则为 false。</param>
        protected override void Dispose(bool disposing)
        {
            if (disposing && (components != null))
            {
                components.Dispose();
            }
            base.Dispose(disposing);
        }

        #region Windows 窗体设计器生成的代码

        /// <summary>
        /// 设计器支持所需的方法 - 不要修改
        /// 使用代码编辑器修改此方法的内容。
        /// </summary>
        private void InitializeComponent()
        {
            this.components = new System.ComponentModel.Container();
            this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
            this.ClientSize = new System.Drawing.Size(800, 450);
            this.Text = "Form1";
        }

        #endregion
    }
}
using System.Windows.Forms;

namespace WindowsFormsApp1
{
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
        }
    }
}

WPF中partial类示例

新建WPF程序,MainWindow窗体类也是一个partial类,他被分成了两部分,一部分为由Xaml语言写的UI设计部分,一部分为由C#语言写的后台逻辑部分,如下:

<Window x:Class="WpfApp1.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:WpfApp1"
        mc:Ignorable="d"
        Title="MainWindow" Height="450" Width="800">
    <Grid>

    </Grid>
</Window>
using System.Windows;

namespace WpfApp1
{
    /// <summary>
    /// MainWindow.xaml 的交互逻辑
    /// </summary>
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
        }
    }
}

ASP.NET Core中partial类示例

新建ASP.NET Core Web应用程序(模型视图控制器),在Views→Home→Index.cshtml中有两部分代码,一部分是C#语言写的,一部分是HTML语言写的,他们是一个partial类的一部分,最终会被编译成C#代码与后台类合并在一起,如下:

@{
    ViewData["Title"] = "Home Page";
}

<div class="text-center">
    <h1 class="display-4">Welcome</h1>
    <p>Learn about <a href="https://docs.microsoft.com/aspnet/core">building Web apps with ASP.NET Core</a>.</p>
</div>

partial虽然在实际很少直接使用,但是它是现代.net编程的基石之一。

枚举类型

基本定义

  • 本质:人为限定取值范围的整数
  • 整数值的对应
  • 用法:比特位式用法

示例

示例:设计一套管理软件,有一个Person类,他有ID、Name和Level属性。可以如下定义:

using System;

namespace HelloEnum
{
    class Program
    {
        static void Main(string[] args)
        {
            Person person = new Person();
            person.Level = "Boss";
        }
    }

    class Person
    {
        public int ID { get; set; }
        public string Name { get; set; }
        public string Level { get; set; }
    }
}

person.Level = “Boss”,这样定义Level值很容易错写大小写或者写错,容易造成bug,使用枚举对Level 进行改造,如下:

using System;

namespace HelloEnum
{
    class Program
    {
        static void Main(string[] args)
        {
            Person person = new Person();
            person.Level = Level.Employee;
            Person boss = new Person();
            boss.Level = Level.Boss;
            Console.WriteLine(boss.Level>person.Level);//结果为True
            Console.WriteLine((int)Level.Employee);//结果为0
            Console.WriteLine((int)Level.Manager);//结果为1
            Console.WriteLine((int)Level.Boss);//结果为2
            Console.WriteLine((int)Level.BigBoss);//结果为3
        }
    }

    enum Level
    {
        Employee,
        Manager,
        Boss,
        BigBoss,
    }

    class Person
    {
        public int ID { get; set; }
        public string Name { get; set; }
        public Level Level { get; set; }
    }
}

当用枚举定义Level时,在访问person的Level,只能在定义的属性中进行选择,此时默认Employee值为0,Manager值为1,Boss值为2,BigBoss值为3,也可以给他们进行赋值,如下:

enum Level
    {
        Employee=100,
        Manager,//未赋值,值默认为101
        Boss=200,
        BigBoss,//未赋值,值默认为201
    }

当Person增加有Skill技能,Skill中有Drive、Cook、Program、Teach四个值,当一个人都具有这4个技能时,应该用比特位式用法,如下:

using System;

namespace HelloEnum
{
    class Program
    {
        static void Main(string[] args)
        {
            Person person = new Person();
            person.Level = Level.Employee;
            person.Name = "Tim";
            person.Skill = Skill.Drive | Skill.Cook | Skill.Program | Skill.Teach;
            //比特位式用法,‘|’为对位取‘或’。
            Console.WriteLine(person.Skill);//结果为15
            Console.WriteLine((person.Skill & Skill.Cook)>0);
            //结果为True,说明person具有Cook技能。‘&’为对位取‘与’。
        }
    }

    enum Level
    {
        Employee,
        Manager,
        Boss,
        BigBoss,
    }

    enum Skill
    {
        Drive=1,//0001
        Cook=2,//0010
        Program=4,//0100
        Teach=8,//1000
    }
    class Person
    {
        public int ID { get; set; }
        public string Name { get; set; }
        public Level Level { get; set; }
        public Skill Skill { get; set; }
    }
}

比特位用法未来使用广泛,如:读写文件时,控制文件的打开方式。串口参数的校验位、停止位。

结构体(struct)

基本定义

  • 结构体类型是值类型,可装箱/拆箱
  • 可实现接口,不能派生自类/结构体
  • 不能有显示无参构造器

示例

值类型特点是,与值类型变量相关联的那块内存里存储的就是值类型的实例,而非像引用类型变量那样存储的是类型实例在堆内存中对应的地址,如下:

using System;

namespace HelloEnum
{
    class Program
    {
        static void Main(string[] args)
        {
            Student student = new Student() { ID = 101, Name = "Tim" };
            //student为局部变量,它是分配在Main函数的函数栈上,此变量所关联的内存直接存储的是Student类型的实例,
            object obj = student;//装箱
            Student student1 = (Student)obj;//拆箱
            Console.WriteLine($"#{student1.ID} Name:{student1.Name}");//结果为:#101 Name:Tim
        }
    }

    struct Student
    {
        public int ID { get; set; }
        public string Name { get; set; }
    }
}

一个值类型变量在赋值为另外一个值类型变量时,并不是引用,而是完整的copy,两个变量在内存中的变量是完全不同的,他们可以独立的变化。如下:

using System;

namespace HelloEnum
{
    class Program
    {
        static void Main(string[] args)
        {
            Student student = new Student() { ID = 101, Name = "Tim" };
            Student student1 = student;
            student1.ID = 1001;
            student1.Name = "Tom";
            Console.WriteLine($"#{student.ID} Name:{student.Name}");//结果为:#101 Name:Tim
        }
    }

    struct Student
    {
        public int ID { get; set; }
        public string Name { get; set; }
    }
}
结构体是可以实现接口的,如下:
using System;

namespace HelloEnum
{
    class Program
    {
        static void Main(string[] args)
        {
            Student student = new Student() { ID = 101, Name = "Tim" };
            student.Speak();//结果为:I'm #101 student:Tim
        }
    }

    interface ISpeak
    {
        void Speak();
    }
    struct Student:ISpeak
    {
        public int ID { get; set; }
        public string Name { get; set; }

        public void Speak()
        {
            Console.WriteLine($"I'm #{this.ID} student:{this.Name}");
        }
    }
}

结构体不能有自己的基类或基结构体,且不能拥有显示的无参构造器,但可以显示显示有参构造器,如下:

struct Student
    {
        public int ID { get; set; }
        public string Name { get; set; }

        public Student(int id,string name)
        {
            this.ID = id;
            this.Name = name;
        }
    }