开发过程中,我们接触到最多的类型是类 Class。C# 中类的命名遵守 Pascal 规则。新的 C# 12.0 引入了一个类初始化的新语法规则,叫主构造器 Primary Constructor 。主构造器简化了类或者结构的定义。请看例子:

public class Person(string name, int age) //primay Constructor
{
    private readonly string _name = name;
    private readonly int _age = age;

    public void DisplayInfo()
    {
        Console.WriteLine($"Name: {name}, Age: {age}");
    }

    public void DisplayPrivateInfo()
    {
        Console.WriteLine($"_Name: {_name}, _Age: {age}");
    }
}

class Program
{
    static void Main()
    {
        var person = new Person("Alice", 30);
        person.DisplayInfo();  
        person.DisplayPrivateInfo(); 
    }
}

主构造器有两个重要特征:

  1. 它定义的参数的作用域是整个类,参数不仅是构造函数的参数,也具有变量的行为,我们可以在类内部读取或者更改它们
  2. 主构造器一旦定义,其他构造函数必须遵循主构造器。例如:主构造器的参数在类初始化时被赋值,其他的构造函数再对这些参数操作时,它们的值已经在那里了。
    这段解释的不好,请多多见谅!

静态 static 成员

静态类通常用途:

  • 应用的公用库,比如,算术计算,操作字符串或文件等
  • 存储全局状态,比如,常数,应用设置
  • 扩展方法,静态类为其他类型定义扩展方法

静态类所有方法也必须是静态方法;因为它是静态的,所以不能有实例成员,属性,字段都要定义为静态。

记录 record

记录 record 是从 C# 9 开始出现的参考类型。设计的初衷主要是为不可变的数据模型提供内建的功能,用于值比较。记录通常被用于创建 DTOs —— 数据传输对象。

  • 如果记录的字段值相同,那么就是同一记录(注意,不是记录相等,是同一)
  • 存储对象不可变
  • 内建 ToString() 方法
  • 支持主构造器和位置语法
  • 用 with 表达式通过拷贝和修改原记录来创建新记录

请看 with 表达式的例子:

public record Book(string Title, string Author);

class Program
{
    static void Main()
    {
        var book1 = new Book("1984", "George Orwell");
        var book2 = book1 with { Title = "Animal Farm" };  // Create a new record with modified Title

        Console.WriteLine(book1);  // Output: Book { Title = 1984, Author = George Orwell }
        Console.WriteLine(book2);  // Output: Book { Title = Animal Farm, Author = George Orwell }
    }
}

结构 struct

记录被用来存储不可变数据,为什么我们还要保留结构 struct 这个类型?因为它是值类型!你可能会反问,这又是什么鬼,反正是存储不可变数据,记录不是就有这个功能吗?我们知道记录是参考类型,意味着它是存储在堆上的;而结构是值类型,它存储在栈上,可以更快存取;没有分配堆空间和垃圾回收的烦恼;所以结构是对简单的数据来说个最理想的存储类型。

类型记录 record结构 struct
存储
比较值比较值比较
性能适合大对象适合简单对象
继承支持继承不支持继承
缺省值null有缺省值

接口

类和结构能够选择实现接口 interface 。
举个栗子:.NET 运行时库定义了一个几乎所有应用都会用到的接口:IEnumerable<T> ,它定义了代表值序列的成员集合。比如,IEnumerable<string> 是字符串序列。这个接口可以和数组,集合一起工作。
当一个类实现了某一个接口时,它就可以被隐式类型转换成这个接口的类型,所以程序就可以通过这个接口的某个表达式指向这个类。
接口是参考类型,尽管我们可以给类和结构实现某个接口,但当给结构实现接口时,C# 会把结构装箱,即构造一个保存有这个结构的拷贝的对象。这个句子中文解释有点绕!我们改用自然语言描述试试看:把拷贝到结构数据打包放在堆上,给它一个参考地址,以后通过参考地址访问它。

缺省接口实现

在.NET 框架中,C# 的接口定义可以包含部分实现,它可以有静态字段,嵌套类型,方法体,属性访问器和添加、移除事件方法。我们先看这个接口:

public interface INamed
{
  int Id { get; }
  string Name => $"NoName with Id:{this.Id}";
}

如果一个类要实现这个接口,它只要实现 Id 这个属性就行了; 这个类也可以定义实现新的 Name 属性,以取代缺省的接口实现。

静态虚拟成员

从 C# 11.0 开始出现一个接口的重要新特性:静态虚拟成员。当接口声明静态方法、属性、事件或自定义操作符时,可以使用 virtual 关键字(在这种情况下,必须提供默认实现)或 abstract 关键字来声明(在这种情况下,必须提供默认实现)或抽象关键字来声明。abstract 和 virtual 关键字是为了与继承保持一致。以这种方式声明的接口成员是静态虚拟成员,它们表示任何实现接口的类型都有相应的静态成员。我们看一个例子,初步了解一下:

public interface ICalculator
{
    // Static abstract member - must be implemented by derived types
    static abstract int Add(int x, int y);

    // Static virtual member - provides a default implementation
    static virtual int Subtract(int x, int y)
    {
        return x - y;
    }
}

public class BasicCalculator : ICalculator
{
    // Implement the static abstract method
    public static int Add(int x, int y)
    {
        return x + y;
    }

    // Optionally override the static virtual method
    public static int Subtract(int x, int y)
    {
        return x - y - 1;  // Custom behavior
    }
}

public class AdvancedCalculator : ICalculator
{
    public static int Add(int x, int y)
    {
        return checked(x + y);  // Ensuring overflow checks
    }

    // Uses the default Subtract() implementation from the interface
}

class Program
{
    static void Main()
    {
        // Call static methods polymorphically
        Console.WriteLine(BasicCalculator.Add(3, 2));         // Output: 5
        Console.WriteLine(BasicCalculator.Subtract(7, 4));    // Output: 2 (custom behavior)

        Console.WriteLine(AdvancedCalculator.Add(10, 20));    // Output: 30
        Console.WriteLine(AdvancedCalculator.Subtract(10, 3)); // Output: 7 (default implementation)
    }
}

这个例子不能体现静态虚拟成员的主要功用,但是可以有助于理解。
静态虚拟成员主要是应用在泛型编程的场景,因为存在泛型静态行为需要被其他类共享。因为是虚拟成员,所以不能直接调用接口里的静态方法,只能在实现接口的类中调用方法。
这个特性主要用在泛型计算算法的情形,一般还用不到。(那… 为什么要学它?)

匿名类型

匿名类型 anonymouse type ,没有名字的类型,但是可以赋值给变量:

var x = new { Title = "Some New Book", Author = "JohnDoe" };
Console.WriteLine($"We published a {x.Title}, the author is {Author}");

编译器会为匿名类型生成一个普通的不可变的类,用它来临时存取一组数据很方便,又有效率。那我们可能就想,为什么不用元组啊? 但是很遗憾,因为现代的代码里会总是用到 Lambda 表达式,然而,元组不能被分解成表达式树,所以 Lambda 不能使用元组。这也许是匿名类存在的最重要的理由。特别的,超级功能 — LINQ 查询要使用 Lambda,它就会经常用到匿名类型。
编译器依据数据存储形态相同,而认为元组 (x: 3, y: 4)(width: 3, height: 4) 相等,即使它们的元素的标签不一样,所以元组更加灵活。
匿名类型会被生成一个类,我们可以推断出 new { x = 3, y = 4 }new { width = 3, height = 4 } 是两个不同的匿名类型。
匿名类型被编译器认为是类,它支持泛型,这是匿名类型比元组更强的特别功能。
匿名类型是不可变类,它支持 with 操作符。

泛型

在 C# 中,我们可以定义类型参数占位符来定义类(结构,记录)、接口、方法和委托,在编译时再插入不同的数据类型。这样,我们就可以只用写一个泛型类型,但可以生成多个数据类型的版本。在处理不同数据类型时,泛型为我们提供了类型安全性和代码重用性,而不会影响性能。

我们先构造一个有主构造器的类:

public class GenClass<T>(T item, string title)
{
  public T Item { get; } = item;
  public string Title { get; } = title;
}

...

var order = new GenClass<int>(2, "Order");
var articleType1 = new GenClass<string>("Jacket", "Clothes");
var articleType2 = new GenClass<string>("Running Shoes", "Shoes");
List<GenClass<string>> productTypes = [articleType1, articleType2];

约束

虽然泛型可以应用其他具体数据类型,但泛型类(方法)不可能能够操作所有的类型,比如泛型方法是数学计算的,而我们插入了一个字符串类型,运行方法代码就会出错了。因此 C# 提供了约束插入的具体类型的机制。

参考类型约束

我们这里利用参考类型约束,构造以通用仓库类,这个类除了参考类型约束,还隐含着非空参考类型约束,因为我们没有指定 class? 。(非空与否还和上下文的 nullable 标注有关)

public class GenericRepository<T> where T : class, new()  // 'T' 必须是具有无参数构造函数的参考类型
{
    public T CreateInstance()
    {
        return new T();  // 因为有了约束,必然安全返回新实例
    }
}

你注意到我的说明里没有说必须是”类“,而是”参考类型“。我们可以在这个泛型占位符插入 类class,接口 interface,代理delegate;也可以插入其他参考类型,比如,string,List<int>,YourClass ... 我觉得这里的 class 我们要理解为编译器认为所有符合 class 定义的东西。

类型约束

我们可以泛型要和特定的类型兼容。这个例子演示了泛型必须是 IComparable<T> 接口:

public class GenericComparer<T> : IComparable<T>
  where T : IComparable<T>
{
  public int Compare(T? x, T? y)
  {
    if(x == null)
    {
      return y == null ? 0 : -1;
    }

    return x.CompareTo(y);
  }
}

值类型约束

在某些情况下,比如:类型安全,提高性能和特别的逻辑要求,我们会使用值类型约束:

public class ValueRepository<T> where T : struct  // Constraint: T must be a value type
{
    private List<T> items = new List<T>();

    public void Add(T item)
    {
        items.Add(item);
    }

    public void DisplayAll()
    {
        foreach (var item in items)
        {
            Console.WriteLine(item);
        }
    }
}

另外还有一些特殊的的约束,如:代理约束、枚举类型约束等不做赘述,还是用到时查相关资料吧。我们也可以给出多个约束:

public class SomeClass<T> 
  Where T: IEnumerable<T>, IDisposable, new()

...

C# 的约束在应用泛型的场合,面对不同数据类型的时候,保证类型安全而不会损失性能。

集合

ASP.NET 的 Web 应用总是需要处理一系列数据,让集合类更好的工作成为最基本的关键技术。常用集合类有:数组 Array, 列表 List,字典Dictionary,集合Set, 队列Queue, 栈Stack,并发集合Concurrent Collection … 我们先从最基本的数组开始

数组

关于数组,我们先讨论 2 个搜索方法:

  • Array.IndexOf() 查找特定的值在数组中的位置,找到就返回它的 Index, 否则返回 -1
  • Array.FindIndex() 根据代理给定的条件查找满足条件的元素,找到也返回 Index, 否则返回 -1
    相比而言,FindIndex() 更加灵活,即使不知道具体查找的值,我们也可以提供逻辑条件查找。
int[] numbers = { 1, 2, -3, 4, -5 };
int firstNegativeNumberIndex = Array.FindIndex(numbers, num => num < 0);

找到一个容易,如果要找到所有元素呢?我们可以请出 FindAll() :

int[] numbers = { 1, 2, -3, 4, -5, 6, 7, 8, 9, 10 };

// 找到所有偶数
int[] evenNumbers = Array.FindAll(numbers, n => n % 2 == 0);

Console.WriteLine("Even Numbers:");
foreach (int number in evenNumbers)
{
    Console.WriteLine(number);
}

这几个看起来很美的方法,如果把它们用于对付大型数组,那么服务器就会累死。怎样才能找的快?先排队!

int[] hugeArray = { ... };
int searchValue = 100;
Array.Sort(hugeArray); //排序
int index = Array.BinarySearch(hugeArray, searchValue);
if (index >= 0)
{
    Console.WriteLine($"Element {searchValue} found at index {index}.");
}
else
{
    // If not found, BinarySearch returns a negative value representing the bitwise complement of the index 
    Console.WriteLine($"Element {searchValue} not found. Insertion point: {~index}");
}

如果二分法找不到呢? 方法会返回一个负数,代表最接近查找目标的元素位置的二进制位补码,我们可以再补一下 ~,得到正数的索引位置。

多重数组,没怎么用到过,略过吧。

列表 List<T>

List<T> 也提供 IndexOf, LastIndexOf, Find, FindLast, FindAll, Sort, 和 Binary Search 这些方法,和数组非常类似,我们用到比较多的是接口,运行时库定义了几个对应于集合类的巨好用的接口。三个最简单的接口是 IList<T>, ICollection<T> 和 IEnumerable<T>。
IEnumerable<T>是其中最最常用的接口,因为它对实现的要求最低。它是这样定义的(忽略了不支持泛型的部分):

public interface IEnumerable<out T> : IEnumerable
{
    IEnumerator<T> GetEnumerator();
}

那神马是 IEnumerator ?它们不会太像了伐!它也是有定义的:

public interface IEnumerator<out T> : IDisposable, IEnumerator
{
  T Current { get; }
}
public interface IEnumerator
{
  bool MoveNext();
  object Current { get; }
  void Reset();
}

IEnumerator 接口有三个方法,当遍历集合的时候,调用 MoveNext() ,如果返回 false,表示是空集合;而 Current 就是当前项,再调用 MoveNext() ,成功返回 true 的同时 Current 就是下一项,如果到底了就返回 false 。Reset() 已经退休了,但是还占着位置。
遍历这些序列的常用语句是 foreach 循环。

IEnumerable<T> 是 LINQ 查询的基本核心之一。也是因为太基本了,它不能修改集合,甚至要得到集合的元素个数,我们都不得不遍历整个序列。对于这些繁重一些的工作,运行时库给我提供了 ICollection<T>,它是这样定义的:

public interface ICollection<T> : IEnumerable<T>, IEnumerable
{
 void Add(T item);
 void Clear();
 bool Contains(T item);
 void CopyTo(T[] array, int arrayIndex);
 bool Remove(T item);
 int Count { get; }
 bool IsReadOnly { get; }
}

这个接口里面的方法,大家望文生义就可以,就不介绍了吧。然后,微软把它们两个糅在一起出个接口,叫 IList<T> :

public interface IList<T> : ICollection<T>, IEnumerable<T>, IEnumerable
{
 int IndexOf(T item);
 void Insert(int index, T item);
 void RemoveAt(int index);
 T this[int index] { get; set; }
}

看到这个接口里方法,我们就知道,数组和 List<T>都是实现这个接口的。

用迭代器实现 IEnumerable<T>

C# 支持一个特别的方法叫迭代器 iterator 。迭代器的关键字是 yield ,是一种处理序列的方法。

static IEnumerable<int> Countdown(int start, int end)
{
 for (int i = start; i >= end; --i)
 {
    yield return i;
 }
}
foreach (int i in Countdown(10, 1)) // 倒数计时
{
    Console.WriteLine(i); 
}

System.Index 和 System.Range

System.Index 与 ^ 操作符配合使用,简化从开头和结尾访问元素的过程。

它通常与 System.Range 结合使用,可灵活切分数组和字符串等集合。

string[] words = { "apple", "banana", "cherry", "date" };

// Using zero-based index
Console.WriteLine(words[1]);  // Output: banana

// Using System.Index with ^ to count from the end
Console.WriteLine(words[^1]); // Output: date
Console.WriteLine(words[^2]); // Output: cherry

Index fromStart = new Index(2);          // Position 2 from start

Console.WriteLine(words[fromStart]);     // Output: cherry

string[] fruits = { "apple", "banana", "cherry", "date", "elderberry" };

// Get a range from 2nd to 4th element
var subset = fruits[1..4]; // banana, cherry, date

foreach (var fruit in subset)
{
    Console.WriteLine(fruit);
}

字典 Dictionary

字段也是最经常用到的集合类型之一,.NET 提供一个字段类:Dictionary<TKey, TValue>,已经另外两个相关的接口:IDictionary<TKey, TValue>IReadonlyDictionary<TKey, TValue> 。在 Web Application 中,特别是和客户端交互数据的形式都可以以这样的键-值的方式存取。还有一个重要的使用场景是缓存(cache)数据,一般也是字典形式。

集合 Set

这个集合的概念和数学中集合比较吻合。一个东西属于集合或者不属于集合。我们将会在设计数据库上下文时,大量的用到这个类型。

冻结集合 Frozen Collections

.NET 7 介绍了一种新的冻结集合,具体说是优化后的只读冻结字典 FrozenDictionary<TKey, TValue> 和冻结集合 FrozenSet<T>。这两个类型适合有了数据后,就不再改变的情形,适用于在集合中快速查找。
我们可以用它们来存储静态配置数据,缓存或者分享数据。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注