这篇文章上次修改于 270 天前,可能其部分内容已经发生变化,如有疑问可询问作者。

泛型,Partial类,枚举,结构

  • 泛型(generic)无处不在

    • 为什么需要泛型: 避免成员膨胀或者类型膨胀

    • 正交性: 泛型类型(类/接口/委托/...),泛型成员(属性/方法/字段/...)

    • 类型方法的参数推断

    • 泛型与委托,lambda表达式

  • Partial类

    • 减少类的派生

    • partial类与Entity Framework

    • partial类与Windows Forms,WPF,ASP.NET Core


泛型使用例其一

泛型与类的正交

现在我们开了一个小商店,我们最开始只卖苹果,用Box来装苹果....

using System;

namespace ConsoleApplication1
{
    class Program
    {
        public static void Main(string[] args)
        {
            Apple apple = new Apple(){Color = "Red"};
            Box box = new Box(){Cargo = apple};
        }
    }

    class Apple
    {
        public string Color { get; set; }
    }

    class Box
    {
        public Apple Cargo { get; set; }
    }
}

随着生意越做越好,商店开始出售更多的商品...

namespace ConsoleApplication1
{
    class Program
    {
        public static void Main(string[] args)
        {
            Apple apple = new Apple(){Color = "Red"};
            Book book = new Book(){Name = "Book"};
            BookBox box1 = new BookBox() { Cargo = book };
            AppleBox box2 = new AppleBox() { Cargo = apple };
        }
    }

    class Apple
    {
        public string Color { get; set; }
    }

    class Book
    {
        public string Name { get; set; }
    }

    class AppleBox
    {
        public Apple Cargo { get; set; }
    }
    class BookBox
    {
        public Book Cargo { get; set; }
    }
}
我们想到用不同的Box来装不同的商品,可是这造成一个问题 -- 类型膨胀

然后我们以这种方式装商品...

using System;

namespace ConsoleApplication1
{
    class Program
    {
        public static void Main(string[] args)
        {
            Apple apple = new Apple(){Color = "Red"};
            Book book = new Book(){Name = "Book"};
            Box box1 = new Box() { Book = book };
            Box box2 = new Box() { Apple = apple };
        }
    }

    class Apple
    {
        public string Color { get; set; }
    }

    class Book
    {
        public string Name { get; set; }
    }

    class Box
    {
        public Apple Apple { get; set; }
        public Book Book { get; set; }
    }

}
这次我们把各种货物变量都作为了Box的属性,可是这遇到又一个问题 -- 成员膨胀

那么....我们以这一种方式呢?

namespace ConsoleApplication1
{
    class Program
    {
        public static void Main(string[] args)
        {
            Apple apple = new Apple(){Color = "Red"};
            Book book = new Book(){Name = "Book"};
            Box box1 = new Box() { Cargo = book };
            Box box2 = new Box() { Cargo = apple };
            Console.WriteLine((box1.Cargo as Book)?.Name);
            Console.WriteLine((box2.Cargo as Apple)?.Color);
        }
    }

    class Apple
    {
        public string Color { get; set; }
    }

    class Book
    {
        public string Name { get; set; }
    }

    class Box
    {
        public Object Cargo { get; set; }

    }

}
我们想到利用C#语言的单根性去使用Object类型变量来引用各种货物...然后又遇到一个问题 -- 访问货物属性困难

于是,我们想到了泛型

namespace ConsoleApplication1
{
    class Program
    {
        public static void Main(string[] args)
        {
            Apple apple = new Apple(){Color = "Red"};
            Book book = new Book(){Name = "Book"};
            Box<Apple> box1 = new Box<Apple>() { Cargo = apple };
            Box<Book> box2 = new Box<Book>() { Cargo = book };
            Console.WriteLine(box1.Cargo.Color);
            Console.WriteLine(box2.Cargo.Name);
        }
    }

    class Apple
    {
        public string Color { get; set; }
    }

    class Book
    {
        public string Name { get; set; }
    }

    class Box<TCargo>
    {
        public TCargo Cargo { get; set; }
    }

}

树

像这样,每次用具体传入类型替换泛型,可以方便的用Box装各种货物并访问它们的属性.

泛型使用例其二

泛型与接口的正交

( 1.1 ) 泛型ID接口
using System;

namespace HelloGeneric
{
    class Program
    {
        public static void Main(string[] args)
        {
            Student<ulong> student = new Student<ulong>();
            student.ID = 10000000000000001;
            student.Name = "Yo";
        }
    }

    interface IUnique<TId>
    {
        TId ID { get; set; }
    }

    class Student<TId> : IUnique<TId>
    {
        public TId ID { get; set; }
        public string Name { get; set; }
    }
}
接口限制学生要有独有的ID属性,但不限制ID的类型.
( 1.2 ) 泛型ID接口特化
using System;

namespace HelloGeneric
{
    class Program
    {
        public static void Main(string[] args)
        {
            Student student = new Student();
            student.ID = 10000000000001;
            student.Name = "Yo";
        }
    }

    interface IUnique<TId>
    {
        TId ID { get; set; }
    }

    class Student : IUnique<ulong>
    {
        public ulong ID { get; set; }
        public string Name { get; set; }
    }
}
学生直接使用特化之后的泛型接口.
(2.1) 泛型特化方法

特化方法

返回值是泛型,传入的参数也是泛型.这里使用Zip方法时不用显式使用<类型>调用方法,因为此时编译器会自动推断.

泛型使用例其三

泛型与委托的正交

(1.1) 泛型委托
using System;
using System.IO.Compression;


namespace HelloGeneric
{
    class Program
    {
        public static void Main(string[] args)
        {
            Func<double,double,double> func1 = Add;
            Func<int,int,int> func2 = Add;
            var result1 = func1(99.55, 200.5);
            var result2 = func2(100, 200);
            Console.WriteLine(result1);
            Console.WriteLine(result2);
        }

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

        static double Add(double a, double b)
        {
            return a + b;
        }
    }
}
此处Add方法有两个重载,特化的泛型委托可以自动找到对应的方法.
(1.2) 泛型委托与lambda表达式的结合使用

lambda

因为func1委托之前引用的方法逻辑简单,可以不需要显式声明,而使用lambda表达式直接创建一个匿名方法让委托引用,避免污染名称空间.

表达

因为委托已经指定了返回值和传入参数的类型,所以可以进一步简化.

Partial类

p1

C#语言中,在同一个程序集中,同一个命名空间下,可以使用Partial关键字把一个类分散在多个文件中实现,之后编译器会自动将它们拼接

p3

P2

Partial类的强大之处

  • 使用Partial类可以将一个类划分为多个部分进行维护,保证一些方法不受另一些方法更新的影响.
  • 使用Partial类不同的Partial可以用不同的编程语言来编写.

WinForm的逻辑部分和设计部分就是分在不同文件的,但使用Partial类,都使用C#语言并且最后会拼接在一起. ヽ( ̄ω ̄( ̄ω ̄〃)ゝ

WPF的界面使用XAML语言编写界面(最后会被编译器编译为C#代码),逻辑使用C#语言.


枚举类型

  • 人为限定取值范围的整数
  • 整数值的对应
  • 比特位式用法
namespace TryEnum
{
    class _Program
    {
        public 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);
            Console.WriteLine((int)Level.Employee);
            Console.WriteLine((int)Level.Manager);
            Console.WriteLine((int)Level.Boss);
            Console.WriteLine((int)Level.BigBoss);
        }
    }



    enum Level
    {
        Employee,
        Manager,
        Boss,
        BigBoss,
    }
    class Person
    {
        public int ID { get; set; }
        public string Name { get; set; }
        public Level Level { get; set; }
    }
}

输出为

True
0
1
2
3

可以指定枚举器背后对应的值

    enum Level
    {
        Employee = 100,
        Manager = 200,
        Boss = 300,
        BigBoss = 400,
    }

如果不给枚举值赋值,会自动在前一个枚举值基础上加一.

比如不给Manager赋值,它的值将会是101.

这就是枚举值和整数之间的对应关系.

初探枚举类型比特位用法

比特位

输出为

True
True
|是按位或,&是按位与.

结构体(struct)

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

结构体可实现接口,但不能有基类或基结构体,而且语法上不允许结构体有显式的不带参数的构造器

而在C# 10更新之后结构体struct有了新特性,结构体可以使用显式的无参构造器了.

不过必须是public, 而且不能是partial.

using System;
using System.IO.Compression;
using System.Threading.Channels;

namespace Try
{
    class _Program
    {
        public static void Main(string[] args)
        {
            Student stu = new Student(){ID = 101,Name="ToT"};
            // stu是一个局部变量 分布在main函数的函数栈上
            object obj = stu; // 将stu copy到堆内存,用obj引用堆内存上的stu实例 此为装箱
            Student stu2 = (Student)obj; // 此处为拆箱
            Console.WriteLine($"#{stu2.ID} Name:{stu2.Name}");

        }
    }

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

结构体为值类型,复制的时候是复制一个完整的对象.见如下代码.

namespace Try
{
    class _Program
    {
        public static void Main(string[] args)
        {
            Student stu1 = new Student() { ID = 101, Name="YO" };
            Student stu2 = stu1;
            stu2.ID = 1001;
            stu2.Name = "Zeng";
            Console.WriteLine($"{stu1.ID},{stu1.Name}");
            Console.WriteLine($"{stu2.ID},{stu2.Name}");

        }
    }

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

输出为:

101,YO
1001,Zeng
  • 由此可见,与引用类型不同,作为值类型的结构体在复制时创建了一个新的对象,进行的修改作用于两个不同的对象,而没有出现像引用类型一样"两个孩子牵着一个气球"(孩子指变量,气球指被引用的对象).

结构体实现接口

using System;

namespace Try
{
    class _Program
    {
        public static void Main(string[] args)
        {
            Student stu1 = new Student() { ID = 101, Name="YO" };
            Student stu2 = stu1;
            stu2.ID = 1001;
            stu2.Name = "Zeng";
            stu1.Speak();
            stu2.Speak();

        }
    }

    interface ISpeak
    {
        void Speak();
    }
    struct Student : ISpeak
    {
        public int ID { get; set; }
        public string Name { get; set; }
        public void Speak()
        {
            Console.WriteLine($"I am #{this.ID} student {this.Name}");
        }
    }
}
再次说明,结构体不能有基类和基结构体.

FROM CHATGPT 3.5

在C#和C++中,结构体(Struct)都是用来表示一组相关数据的自定义类型,但它们在语法和行为上有一些区别。

C#中的结构体和C++中的结构体区别:

C#中的结构体(Struct):

  1. 值类型: 在C#中,结构体是值类型,意味着当你将结构体赋值给另一个变量或作为参数传递时,实际上是在复制整个结构体的值。这与类不同,类是引用类型,赋值和传递时传递的是引用。
  2. 继承不支持: C#中的结构体不支持继承,它们不能从其他结构体或类派生。它们只能实现接口。
  3. 默认构造函数: 如果你没有为结构体定义任何构造函数,C#会为你提供一个默认的无参数构造函数。但你也可以自定义构造函数。
  4. 堆栈和堆: 结构体的实例可以存储在堆栈上,也可以存储在堆上,具体取决于它们是在哪里创建的。在C#中,如果在堆上创建结构体实例,需要使用new关键字。

C++中的结构体:

  1. 值类型: 在C++中,结构体也是值类型。和C#一样,将结构体赋值给另一个变量会复制整个结构体的值。
  2. 继承支持: C++中的结构体支持继承,可以从其他结构体或类派生。
  3. 默认构造函数: 如果你没有为结构体定义任何构造函数,C++会提供一个默认的无参数构造函数。你也可以自定义构造函数。
  4. 内存布局: 在C++中,结构体的内存布局和类非常相似,它们可以包含成员变量、函数等,而在C#中,结构体的成员限制较多。

C++中的结构体和类区别:

在C++中,结构体(struct)和类(class)的主要区别在于默认的成员访问权限:

  1. 默认访问权限: 在类中,默认的成员访问权限是私有(private),而在结构体中,默认的成员访问权限是公有(public)。
  2. 继承: 在类中,默认继承是私有继承(private inheritance),而在结构体中,默认继承是公有继承(public inheritance)。

这些差异导致在使用时,结构体更适合简单的数据集合,而类更适合包含更多行为和复杂逻辑的对象。在C++中,除了上述差异,结构体和类基本上是相似的,可以互相替代。