>> >> >> Reference << << << <<<<<<Ref>>>>>>
Miscellaneous
Modified: 2025-06-01 | Author:ljf12825

using

using在C#中有两种主要用法:作为指令(namespace导入)和作为语句(资源管理)

using指令(Namespace导入)

用于导入命名空间,简化类型访问

using System; // 导入 System命名空间
using System.IO; // 导入 System.IO命名空间
using Project.Models; // 导入自定义命名空间

// 使用示例
Console.WriteLine("Hello"); // 不需要写 System.Console.WriteLine

特殊用法

  1. 静态using(C# 6+)
using static System.Math; // 导入静态类
var x = Sqr(16); // 可直接使用Sqrt而不是Math.Sqrt
  1. 别名using
using WinForms = System.Windows.Forms;
var form = new WinForms.Form(); // 使用别名

using语句(资源管理)

用于自动管理实现了IDisposable接口的对象资源,确保对象在使用后被正确释放

using (var resource = new DisposableObject())
{
    // 使用 resource
} // 自动调用 resource.Dispose()

示例

  1. 文件操作
using (var file = new StreamWriter("test.txt"))
{
    file.WriteLine("Hello");
} // 自动关闭文件
  1. 数据库连接
using (var conn = new SqlConnection(connectingString))
{
    conn.Open();
    // 执行数据库操作
} // 自动关闭连接
  1. 多个资源
using (var res1 = new Resource1())
using (var res2 = new Resource2())
{
    // 使用 res1和res2
} // 先释放 res2 再释放 res1

C# 8.0简化写法

using var file = new StreamWriter("test.txt");
file.WriteLine("Hello");
// 当离开当前作用域时自动释放

底层原理 using语句会被编译器转换为类似以下结构

{
    var resource = new DisposableObject();
    try
    {
        // 代码块
    }
    finally
    {
        if (resource != null)
            resource.Dispose();
    }
}

使用注意事项

  1. 必须实现IDisposable
  2. 异常处理
using (var resource = new DisposableObject())
{
    // 即使这里抛出异常,Dispose()也会被调用
}
  1. 不要重复释放
var resource = new DisposableObject();
using (resouce)
{
    // ...
}
// 这里不要再调用 resource.Dispose()
  1. 异步场景 C# 8.0+ 支持异步using
await using (var resource = new AsyncDisposableObject())
{
    // 异步操作
}

适用场景

default(T)

default(T)是一个运算符,它返回类型T的默认值
它的作用就是:无论T是什么类型,都给我一个该类型最“默认”、最“基础”的值

默认值规则

default(T)的行为取决于T是值类型还是引用类型,遵循C#语言的默认值规则:

  1. 对于所有引用类型(classinterfacedelegatearraystring等)
  1. 对于值类型(struct和所有数字类型、boolcharenum等)

default(T)的存在意义

主要原因是泛型。在编写泛型类后方法时,编译器在编译时无法知道类型参数T具体是值类型还是引用类型
假设没有default(T),可能会这样写

public T GetDefaultValue<T>()
{
    // 如果T是引用类型,返回null是OK的
    // 如果T是值类型,null不是有效值,编译错误
    return null; // CS0403:无法将null转换为类型参数T
}

或者

public T GetDefaultValue<T>()
{
    // 如果T是引用类型,返回0没有意义
    //编译错误
    return 0; // CS0029:无法将类型 int 隐式转换为T
}

default(T)优雅地解决了这个问题。它让编译器根据具体的T在运行时决定返回null还是“归零”的值类型实例

实际应用场景和示例

  1. 泛型类和方法的初始化 这是default(T)最经典的使用场景
public class DataStroe<T>
{
    private T _data;

    public DataStore()
    {
        // 在构造函数中,将_data初始化为T的默认值
        // 如果T是 int, _data = 0
        // 如果T是 string _data = null
        _data = default(T);
    }

    public bool IsDataPresent()
    {
        // 比较时也需要 default(T)
        // 对于引用类型,这是与null比较
        // 对于值类型,这是与0/false等比较
        return !EqualityComparer<T>.Default.Equals(_data, default(T));
    }

    // 使用
    var intStore = new DataStore<int>(); // _data初始化为0
    var stringStore = new DataStore<string>(); // _data 初始化为 null
}
  1. 方法的默认返回值 当一个泛型方法需要返回一个“无结果”或“初始”值时
public T FindItem<T>(List<T> list, Predicate<T> predicate)
{
    foreach (var item in list) if (predicate(item)) return item;
    // 如果没找到,返回T的默认值
    // 对于引用类型返回null,对于值类型返回0
    return default(T);
}

// 使用
List<string> names = new List<string> { "Alice", "Bob" };
var result = FindItem(names, n => n == "Charlie"); // result 为 null

List<string> numbers = new List<int> { 1, 2, 3 };
var result2 = FindItem(numbers, n => n > 5); // result2 为 0
  1. 重置或清除值
public void ClearValue<T>(ref T value) => value = default(T); // 将value重置为其类型的默认状态

defalut字面量(C#7.1引入)

从C# 7.1开始,编译器变得足够智能,可以根据上下文推断出T的类型。因此,可以省略<T>,只写default,使代码更简洁

// 之前的写法
int num = default(int);
string str = default(string);
MyGenericMethod<string>(default(string));

// C# 7.1+ 的推荐写法
int num = default; // 编译器知道你要的是 int,所以是 0
string str = default; // 编译器知道你要的是 string,所以是 null
MyGenericMethod<string>(default); // 编译器根据方法参数类型推断出是 default(string)

// 在泛型方法中同样适用
public T GetDefault() => default; // 等价于 default(T)

Expression-bodied

表达式主体是一个从C# 6开始引入并逐步增强的语法糖,旨在让代码更简洁、更易读

核心概念

表达式主题允许使用箭头符号=>来替代传统的大括号{}代码块,将一个成员的定义简化为单个表达式
它的核心思想是:如果一个成员(如方法、属性)的逻辑可以在一行表达式内完成,那么就可以使用这种简洁的语法

历史演变

EditionSupport
6.0方法、只读属性、运算符
7.0构造函数、终结器、Getter、Setter、索引器的访问器(get/set)
9.0顶级语句(本质上是Main方法的表达式主体)

示例

  1. 方法(Methods)- C# 6+
// 传统写法
public string GetFullName(string firstName, string lastName)
{
    return $"{firstName} {lastName}";
}

// 表达式主题写法
public string GetFullName(string firstName, string lastName) => $"{firstName} {lastName}";
  1. 只读属性(Read-only Properties)- C# 6+
//传统写法
public DateTime CreatedTime
{
    get { return DateTime.Now; }
}

// 表达式主体写法
public DateTime CreatedTime => DateTime.Now;
  1. 属性访问器(Property Accessors)- C# 7+ C# 7.0允许对属性的getset访问器单独使用表达式主体
private string _firstName;

// 传统写法
public string FirstName
{
    get { return _firstName; }
    set { _firstName = value; }
}

// 表达式主体写法
public string FirstName
{
    get => _firstName;
    set => _firstName = value;
}
  1. 构造函数和终结器(Constructors and Finalizers)- C#7+
public class Person
{
    private string _name;

    // 传统构造函数
    public Person(string name)
    {
        _name = name;
    }

    // 表达式主体写法
    public Person(string name) => _name = name;

    // 传统终结器(析构函数)
    ~Person()
    {
        Console.WriteLine("Finalized");
    }

    // 表达式主体终结器
    ~Person() => Console.WriteLine("Finalized");
}

注意:构造函数通常有多个语句(参数验证、初始化等),所以旨在其逻辑非常简单(如直接赋值)时才适合表达式主体

  1. 索引器(Indexers) - C# 7+
private string[] _items = new string [10];

// 传统写法
public string this[int index]
{
    get { return _items[index]; }
    set { _items[index] = value; }
}

// 表达式主体写法
public string this[int index]
{
    get => _items[index];
    set => _items[index] = value;
}

使用表达式主体的优点

  1. 简洁性(Brevity):这是最主要的目的。它显著减少了样板代码(如大括号、return关键字、get/set块),让代码行数更少,更紧凑
  2. 可读性(Readability):对于简单的成员,表达式主体就像数学公式一样一目了然,意图非常清晰。一眼就能看出“这个属性返回什么”或“这个方法做什么”
  3. 函数式风格(Functional Style):它鼓励将逻辑编写为简单的表达式链,而不是复杂的语句块,使代码更接近函数式编程风格
  4. 与Lambda表达式一致:语法上与Lambda表达式(x) => x * x保持一致,降低了学习成本,让语言更统一

注意事项与缺陷

  1. 仅限单个表达式:这是最大的限制。表达式主体只能包含一个表达式,不能包含语句(如ifswitchfortry-catch等)
  1. 调试略有不同:在调试时,整个表达式主体被视为一行代码。无法像在代码块中那样在return前设置断点。但这通常不是大问题

  2. 不要过度使用:简洁不应以牺牲可读性为代价

var

var是C# 3.0引入的一个非常重要的关键字,它允许开发者在声明变量时让编译器自动推断变量的类型

基本语法和使用

// 显式类型声明
string explicitName = "John";
int explicitAge = 25;

// 使用var隐式类型声明
var implicitName = "John"; // 编译器推断为string
var implicitAge = 25; // 编译器推断为 int
var implicitList = new List<string>; // 编译器推断为 List<string>

工作原理

var的工作原理实际上是基于C#的类型推断机制
编译器通过分析var右侧的表达式来推导处变量的类型
这种推断发生在编译阶段,因此var变量的类型在运行时与显式声明的类型是完全一致的
尽管C#是一种强类型的语言,var只是简化了变量声明的过程,但最终类型依然是静态的,且在编译时已知
目的是提高开发效率、减少代码冗余,而不是改变C#的类型系统
var在性能上是零成本的,因为在编译期推断,生成的IL代码与显式声明完全相同

适用场景

  1. 复杂类型声明
// 没有 var 的情况
Dictionary<string, List<Dictionary<int, string>>> complexDict = new Dictionary<string, List<Dictionary<int, string>>>();

// 使用 var 的情况
var complexDict = new Dictionary<string, List<Dictionary<int, string>>>();
  1. LINQ查询结果
var results = from person in people
                where person.Age > 18
                select new { person.Name, person.Age };

// 等价于
IEnumerable<<anonymous type>> results = ...;
  1. 匿名类型
var person = new { Name = "John", Age = 30 };
Console.WriteLine($"{person.Name} is {person.Age} years old");

// 没有 var, 匿名类型无法显式声明

不适用场景

  1. 不能用于字段声明
public class MyClass
{
    // private var myField; // 编译错误!
    private int myField; // 正确
}
  1. 必须初始化
var value; // 编译错误!必须初始化
value = 10;

var value = 10; // 正确
  1. 不能为null的初始化
var value = null; // 编译错误!无法推断类型

// 解决方案
string value = null;
var value = (string)null;
var value = default(string);

类型推断规则

  1. 字面量推断
var i = 10; // int
var d = 10.5; // double
var f = 10.5f; // float
var m = 10.5m; // decimal
var s = "hello"; // string
var b = true; // bool
  1. 表达式推断
var result = GetResult(); // 类型取决于方法的返回类型

var sum = 5 + 3.2;
var concat = "Age: " + 25; // string

示例

  1. 集合操作
var numbers = new List<int> { 1, 2, 3, 4, 5 };

// 使用 var 让代码更简洁
var evenNumbers = numbers.Where(n => n % 2 == 0).ToList();
var squareNumbers = numbers.Select(n => n * n).ToList();

// 对比显式声明
List<int> evenNumbersExplicit = numbers.Where(n => n % 2 == 0).ToList();
  1. 异步编程
public async Task ProcessDataAsync()
{
    // 使用 var 简化异步调用
    var result = await GetDataAsync();
    var processed = await ProcessResultAsync(result);

    // 对比显式声明
    DataResult resultExplicit = await GetDataAsync();
    ProcessedData processedExplicit = await ProcessResultAsync(resultExplicit);
}
  1. 模式匹配(C# 7.0+)
public void ProcessObject(object obj)
{
    // 使用 var 模式
    if (obj is var str && str is string)
    {
        Console.WriteLine($"String length: {str.Length}");
    }

    // 使用 var 在 switch 表达式中国
    var result = obj switch
    {
        string s => $"String: {s}",
        int i => $"Int: {i}",
        var unknown => $"Unknown: {unknown?.GetType().Name}"
    };
}

最佳实践

  1. 在类型明显时使用var
  2. 在复杂类型声明时使用var提高可读性
  3. 在LINQ查询和匿名类型中必须使用var
  4. 在类型不明显时考虑使用显式以提高代码可读性
  5. 团队应该制定一致的var使用规范

dynamic

dynamic是C#4.0中引入的关键字,它的核心目的是绕过编译时的类型检查,将类型解析的工作推迟到运行时

静态类型 vs 动态类型

要理解dynamic,首先要明白C#本质上是一种静态类型语言

基本用法和语法

dynamic myVariable = 10; // 开始时是整数
Console.WriteLine(myVariable); // 输出:10

myVariable = "Hello, World!"; // 现在变成了字符串
Console.WriteLine(myVariable); // 输出:Hello, World!

myVariable = new List<int>(); // 现在又是一个列表

一个dynamic变量在其生命周期内可以指向不同类型的对象

使用场景

  1. 与COM互操作(如Office自动化) 这是dynamic被引入的首要原因。早期操作Excel或Word时,代码非常冗长,需要大量强制转换
    没有dynamic
var excelApp = new Microsoft.Office.Interop.Excel.Application();
excelApp.Visible = true;
// 需要强制转换,而且参数是 ref object,很麻烦
Microsoft.Office.Interop.Excel.Workbook workbook = (Microsoft.Office.Interop.Excel.Workbook)excelApp.Workbooks.Add();
Microsoft.Office.Interop.Excel.Worksheet worksheet = (Microsoft.Office.Interop.Excel.Worksheet)workbook.ActiveSheet;
Microsoft.Office.Interop.Excel.Range range = (Microsoft.Office.Interop.Excel.Range)worksheet.Cells[1, "A"];
range.Value2 = "Hello";

使用dynamic

dynamic excelApp = new Microsoft.Office.Interop.Excel.Application();
excelApp.Visible = true;
dynamic workbook = excelApp.Workbooks.Add();
dynamic worksheet = workbook.ActiveSheet;
worksheet.Cells[1, "A"].Value = "Hello"; // 代码简洁明了,像脚本语言一样
  1. 与动态语言(IronPython, IronRuby)交互 当需要在C#中调用IronPython或IronRuby等动态语言编写的代码时,dynamic是完美的桥梁
// 示例:在 C# 中执行 Python 代码
var engine = Python.CreateEngine();
dynamic scope = engine.CreateScope();
engine.ExecuteFile("my_script.py", scope);

// 调用 Python 脚本中定义的函数
dynamic result = scope.MyPythonFunction(42);
Console.WriteLine(result);
  1. 处理动态JSON或XML(反序列化位置结构的数据) 当不确定反序列化后的JSON结构时,可以使用dynamic来轻松访问数据
    使用Newtonsoft.Json(Json.NET)
string json = @"{
    'Name': 'Alice',
    'Age': 30,
    'Pets': ['Dog', 'Cat']
}";

// 反序列化为 dynamic
dynamic data = JsonConvert.DeserializeObject(json);

Console.WriteLine(data.Name); // 输出: Alice
Console.WriteLine(data.Age);  // 输出: 30
Console.WriteLine(data.Pets[0]); // 输出: Dog

// 甚至可以处理运行时才存在的属性
if (data.Hobbies != null) { // 如果JSON中没有Hobbies属性,这里不会编译错误
    Console.WriteLine(data.Hobbies);
}
  1. 模拟鸭子模型 “鸭子模型”是动态语言中的一个概念:“如果它走起来像鸭子,叫起来像鸭子,那么它就是鸭子。”即只关心对象有没有某个方法或属性,而不关心它的具体类型
public void MakeSound(dynamic animal)
{
    // 只要在运行时,animal有Quack方法,这行代码就能成功
    animal.Quack();
}

// 这两个类没有继承自同一个接口或基类
public class Duck
{
    public void Quack() => Console.WriteLine("Quack!");
}

public class Person
{
    public void Quack() => Console.WriteLine("人叫");
}

// 使用
MakeSound(new Duck()); // 成功
MakeSound(new Person()); // 成功
// MakeSound(new Dog()); // 如果Dog没有Quack方法,运行时这里会抛出异常

优缺点

优点

缺点

dynamicvar的区别

特性vardynamic
类型决定时间编译时运行时
类型安全是,编译器推断出类型后,就等同于该类型否,编译器不做检查
智能感知
是否可以重新赋值为不同类型

!(null-forgiving. 空值忽略符)

告诉编译器“确信这里不为空,不要再报可能为null的警告”

示例

Type? t = typeof(string);
object? obj = Activator.CreateInstance(t); // 可能返回 null 

C#的可空类型检查系统(nullable reference types)会在这里发出警告:“Activator.CreateInstance可能返回null”
如果这样写

object instance = Activator.CreateInstance(t)!;

编译器会认为:“开发者负责保证这不是null,不再发出警告”

实际行为

!只影响编译器的可空检查,对运行时完全没有任何效果
如果结果真的为null,程序依然会NullReferenceException崩溃
这可能会掩盖潜在的bug,建议只在非常确信值不可能为null时使用

_(Discard,弃元运算符)

switch语句中作为弃元

在C#7.0引入的模式匹配中,_switch语句里充当“默认”或“匹配所有“的案例

object obj = 42;

switch (obj)
{
    case string s:
        Console.WriteLine($"这是一个字符串:{s}");
        break;
    case int i when i > 0:
        Console.WriteLine($"这是一个正整数:{i}");
        break;
    case int i:
        Console.WriteLine($"这是一个整数:{i}");
        break;
    case null:
        Console.WriteLine($"这是 null");
        break;
    case var _: // 使用_来匹配任何其他情况,但不关心匹配到的值
        Console.WriteLine("未知的类型");
        break;
    // 传统的 default: 也可以,但 case var _: 更侧重于“不关心值”
}

在这里,case var _:捕获了所有未被前面案例处理的情况,并且明确表示不关心这个匹配到的对象是什么。它比default在语义上更强调“忽略”

在元组和结构中作为弃元

当使用元组或进行结构操作时,经常只对其中一部分值感兴趣。_可以用来忽略那些不关心的部分

// 返回一个元组的方法
(string Name, int Age, string City) GetPersonInfo()
{
    return ("Alice", 30, "New York");
}

// 只关心姓名和你那零,不关心城市
var (name, age, _) = GetPersonInfo();
Console.WriteLine($"{name} is {age} years old.");

// 甚至可以忽略多个值
var (firstName, _, _) = GetPersonInfo();

这使代码非常简洁,避免了为不需要的变量起名字

在Out参数中作为弃元

在调用带有out参数的方法时,如果不管关心那个输出值,可以使用_来忽略它。这从C#7.0开始被支持

// 例如 int.TryParse 方法
string input = "123"

// 旧方式:即使不关心结果,也必须声明一个变量
int unused;
if (int.TryParse(input, out unused))
    Console.WriteLine("解析成功!");

// 新方式:使用_弃元,代码更清晰
if (int.TryParse(input, out_))
    Console.WriteLine("解析成功!");

在Lambda表达式中作为参数

在Lambda表达式中,如果某个参数不被使用,可以将其命名为_,以明确表示该参数被故意忽略

// 例如,一个按钮的点击事件,不需要使用 EventArgs 参数
button.Click += (_, _) => Console.WriteLine("Button clicked!");

// 或者对于 Func/Action,如果有多个参数但只能使用一个
Action<int, int> action = (_, value) => Console.WriteLine(value);
action(1, 2); // 只输出 2

注意:在同时忽略多个参数时(如(_, _)),这是允许的。但如果只忽略一个参数,而使用另一个,编译器是能区分开的

作为私有字段的命名前缀(约定俗成)

这虽然不是语言特性,但是在C#社区中也给非常广泛和重要的编码约定
在定义类的私有字段时,很多人喜欢在其名字前加上_作为前缀,以便于将其与同名的局部变量(尤其是构造函数或属性设置器中的参数)区分开来

public class Person
{
    private string _name; // 私有字段
    private int _age;

    public Person(string name, int age)
    {
        _name = name; // 清晰地区分了参数 name 和字段 _name
        _age = age;
    }

    public string Name
    {
        get { return _name; }
        set { _name = value; }
    }
}

这个约定极大地提高了代码的可读性,让人一眼就能看出一个标识符时类的私有字段还是一个局部变量

在数字字面量中作为数字分隔符

从C#7.0开始,_可以在数字字面量中作为分隔符使用,以提高大数字的可读性。它不会影响数字的值

long bigNumber = 1_000_000_000; //1B
double pi = 3.141_592_653_589_793;
uint hex = 0xDE_AD_BE_EF;
byte binary = 0b1101_0101;

这使得数字更容易被人阅读和理解

where & when

switch表达式或语句中的when

C#7.0引入模式匹配(pattern matching)后,when可以用来对匹配成功的模式再加一道过滤逻辑

switch (obj)
{
    case int n when n > 0:
        Console.WriteLine("正整数");
        break;
    case int n when n < 0:
        Console.WriteLine("负整数");
        break;
    case int n:
        Console.WriteLine("0");
        break;
    case string s when s.Length > 5:
        Console.WriteLine("长字符串");
        break;
    default:
        Console.WriteLine("其他类型");
        break;
}

这里when起的作用就是:只有模式匹配成功,并且when后面的表达式为true时,这个case才会命中
它让switch语句从原来的“静态匹配”变成了“模式 + 条件”的组合,就像给case增加了一个“守卫”(guard)

catch子句中的when

它允许在捕获异常时有条件地决定是否处理该异常

try
{
    DangerousOperation();
}
catch (IOException ex) when (ex.Message.Contains("disk"))
{
    Console.WriteLine("磁盘相关的IO错误");
}
catch (IOException ex)
{
    Console.WriteLine("其他IO错误");
}

这样做的好处是:不需要在catch块内部再去if (...) throw;
直接在when过滤阶段就能决定是否进入该块

在异常处理中使用when是非常优雅的方式,因为它不会吞掉异常链的信息(不像try-catch-if-throw那样会重新抛出而改变栈信息)

where泛型约束

1. 限制类型必须实现某接口

void Print<T>(T obj) where T : IDisposable => obj.Dispose();

T必须实现IDisposable,否则编译不通过

2. 限制类型必须继承某类

class Base {}
class Derived : Base {}

void Show<T>(T value) where T : Base => Console.WriteLine("T继承自Base");

3. 限制类型必须有无参构造函数

void Create<T>() where T : new() => var obj = new T();

4. 多重约束(可以叠加)

void DoSomething<T>() where T : BaseClass, IInterface, new()
{
    // ...
}

5. 约束多个泛型参数

class Manager<T, U>
    where T : class
    where U : struct
    { }

record

在C#9之前,只能用class或struct

类型语义特点
class引用语义比较地址(引用),可变
struct值语义拷贝传递,性能好但语法笨重

如果只想表达“一组数据”,比如Person(Name, Age),但不想写一堆样板代码(EqualsToStringGetHashCode),而且希望它是不可变的(immutable),同时希望比较的是内容相等,不是内存引用。所以微软在C#9.0引入了record

基本语法

public record Person(string Name, int Age);

这行代码等价于

public class Person
{
    public string Name { get; init; }
    public int Age { get; init; }

    public Person(string Name, int Age)
    {
        this.Name = Name;
        this.Age = Age;
    }

    public override string ToString() => $"Person {{ Name = {Name}, Age = {Age} }

    public override bool Euqals(object? obj) => obj is Person other && Name == other.Name && Age == other.Age;

    public override int GetHashCode() => HashCode.Combine(Name, Age);
}

record会自动

  1. 定义构造函数
  2. 定义只读属性(init
  3. 实现EqualsGetHashCode(按值相等)
  4. 生成友好的ToString()

值相等(Value Equality)

var p1 = new Person("Alice", 18);
var p2 = new Person("Alice", 18);

Console.WriteLine(p1 == p2); // True

区别于class

这也是它被称为“record type(记录类型)”的原因

不可变对象(Immutable)

record默认使用init访问器,而不是set

var p = new Person("Alice", 18);
// p.Age = 10; // 不允许修改

但可以通过with表达式复制并修改

var p2 = p with { Age = 19 };

这会创建一个新对象,原对象不变

with表达式(复制表达式)

这是record的灵魂搭档

var p1 = new Person("Alice", 18);
var p2 = p1 with { Name = "Bob" };

Console.WriteLine(p2); // Person { Name = Bob, Age = 18 }

它相当于

var p2 = new Person(p1.Name, p1.Age);
p2.Name = "Bob";

继承与Record Class / Record Struct

默认

public record Person(string Name);

等价于public record class Person(string Name)
如果想要值类型(避免堆分配)

public record struct Point(int X, int Y)

解构(Deconstruction)

var person = new Person("Alice", 18);
var (name, age) = person;
Console.WriteLine($"{name}, {age}");

C#会自动生成Deconstruct方法

public void Deconstruct(out string Name, out int Age)
{
    Name = this.Name;
    Age = this.Age;
}

继承与Record层次结构

Record支持继承,并保持值相等的正确行为

public record Person(string Name);
public record Student(string Name, int Grade) : Person(Name);

var s1 = new Student("Alice", 1);
var s2 = new Student("Alice", 1);

Console.WriteLine(s1 == s2); // True

但注意:

record的相等比较是基于类型 + 内容的 即使内容相同,不同类型也不相等

可变record(不推荐)

可以改成这样,但失去了不可变的优势

public record Person
{
    public string Name { get; set; }
    public int Age { get; set; }
}

底层原理(编译后)

编译器实际上生成的是一个普通类,只是自动生成了

这意味着也可以重写这些方法来自定义行为

and, or, not

C#9.0引入逻辑模式匹配关键字:and, or, not
它们不是用来替代&&, ||, |,而是只在模式匹配(pattern matching)上下文中生效

背景:模式逻辑表达式的自然语言化

在传统C#中

if (x > 0 && x < 10)

这是典型的逻辑运算符(operators)
当当模式匹配出现后,可以写出

if (x is > 0 and < 10)

这时候and, or, not并不是操作符,而是模式组合符
它们属于pattern syntax,而不是boolean logic

语法规则

关键字对应符号含义适用场景
and&&两个模式都要匹配模式组合
or``任意一个模式匹配即可模式选择
not!匹配模式取反模式否定

这些关键字只能出现在pattern matching表达式中(isswitch),不能在普通布尔逻辑中替代符号形式

  1. and——逻辑与(组合两个模式)
int x = 7;

if (x is > 0 and < 10)
    Console.WriteLine("在0~10之间");

等价于

if (x > 0 && x < 10)

and的强大之处在于:它可以组合任意两种“模式”,不仅限于数值比较

object obj = 42;

if (obj is int n and > 0)
    Console.WriteLine($"正整数 {n}");
  1. or——逻辑或(匹配任意一个模式)
int x = 0;

if (x is < 0 or > 100)
    Console.WriteLine("超出范围");

等价于

if (x < 0 || x > 100)

同样可以组合不同类型的模式

object obj = "Hello";

if (obj is int or string)
    Console.WriteLine("是数字或字符串");
  1. not——模式取反
object obj = null;

if (obj is not null)
    Console.WriteLine("不是 null");

等价于

if (obj != null)

也可以取反整个复杂模式

if (x is not (>0 and < 10))
    Console.WriteLine("不在 0~10 之间");

C#逐步在向“模式导向语言”转型。模式匹配的语法希望更自然、可读、接近英语表达

is/as

isas是两个用于类型转换和类型检查的关键字

is

is用来检查对象是否是某个类型的实例,它返回一个布尔值,表示指定对象是否能够成功转换为目标类型

object obj = "Hello, world";
bool result = obj is string; // true, typeof(obj) == string

C#7.0之后,is可以同时进行类型检查和类型转换,这被称为模式匹配。如果类型匹配,obj会自动转换为目标类型,如果不匹配,不会进行转换

object obj = "Hello, world!";
if (obj is string str) Console.WriteLine(str);

as

as是安全的转换,将对象转换为指定的目标类型,as在转换时不会抛出异常,如果转换成功,返回目标类型的对象;如果失败,返回null

object obj = "Hello, world";
string str = obj as string; // str 是 ”Hello, world“

对于值类型,如intstruct等,as不能用于值类型转换,如果尝试将一个值类型转换成另一个值类型,as将无法工作,编译时会报错

 float f1 = 2.3f;
        double d1 = 3.33;

        double d2 = f1 as double;
        float f2 = d1 as float; // 编译错误

为什么as对值类型没有作用
as 在底层实际上执行的是 引用类型的安全转换。它试图将一个对象引用转换为目标类型,如果目标类型不匹配,它会返回 null。但对于值类型,as 没有办法做这种转换,因为值类型本身没有像引用类型那样的 类型兼容性

is vs as

特性is 关键字as 关键字
功能类型检查,并且可以做类型转换(模式匹配)安全的类型转换,如果失败返回 null
返回值truefalse(布尔值)转换后的目标类型(或 null
转换失败时行为如果转换失败,返回 false转换失败时返回 null
使用场景用于检查对象是否符合某类型,适用于类型匹配检查用于安全的类型转换,避免异常抛出
适用类型适用于所有类型(包括值类型和引用类型)只适用于引用类型(classinterface,不能用于值类型)
性能性能稍差,进行类型检查性能较好,不会抛出异常(适合频繁调用)
  1. 类型检查与转换
  1. 适用场景
  1. 性能差异

使用场景示例

场景1:类型检查与转换is
假设你有一个动物类Animal和它的派生类Dog,你想检查一个Animal对象是否是Dog类型

class Animal { }
class Dog : Animal { }

Animal animal = new Dog();
if (animal is Dog dog)
    Console.WriteLine("dog");

这里的 is 既是类型检查,又进行了类型转换,animal 被转换为 Dog 类型,赋值给变量 dog

场景2:安全的类型转换as
假设你有一个对象,它可能是某个类型,也可能不是,你希望安全地转换它

object obj = "Hello, world!";
string str = obj as string;
if (str != null) Console.WriteLine("success");
else Console.WriteLine("default");

跳转指令

  1. break
    • 作用:立即退出当前循环(for/while/do while / switch
for (int i = 0; i < 10; i++)
{
	if (i == 5)
		break; // 当 i == 5 时退出循环
	Console.WriteLine(i); 
}
// 输出 0, 1, 2, 3, 4
- 只会跳出当前层级的循环
- 常用于提前结束循环,比如找到目标后不再继续搜索
  1. continue
    • 作用:跳过当前这次循环,直接进入下一次循环迭代
for (int i = 0; i < 5; ++i)
{
	if (i == 2)
		continue; // 跳过 i == 2 这次循环
	Console.WriteLine(i);
}
// 输出 0, 1, 3, 4
- `continue`不会终止循环,只是“跳过这次”
- 通常用于“跳过不需要处理的情况”
  1. goto
    • 作用:跳转到代码中带有特定标签的位置
int x = 0;
start: // 标签
Console.WriteLine(x);
x++;
if (x < 3)
	goto start; // 跳回标签处
// 输出 0, 1, 2
- `goto`是“无条件跳转”,强大但危险
- 会破坏结构化编程的逻辑流,不建议随便用
- 唯一合理使用场景:
  - 从嵌套循环中跳出多层;
  - 或在`switch`中做复杂分支跳转
  1. return
    • 作用:立即结束当前方法的执行,并可选择性地返回一个值 无返回值
void PrintNumbeer(int n)
{
	if (n < 0)
		return; // 提前结束函数
	Console.WriteLine(n);
}

有返回值

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

yield return

yield return是C#提供的一种语法糖,用于快速、方便地创建迭代器

简单来说,它让你能像写普通方法一样,写一个能“记住”执行状态的方法,每次调用只返回下一个值

执行模型

当写一个含有yield return的方法

IEnumerable<int> Foo() {
    yield return 1;
    yield return 2;
    yield return 3;
}

编译器不会按字面理解那样生成一个返回值列表
编译器会偷偷把这个方法编译成一个隐藏类,这个类实现了IEnumerable<T>IEnumerator<T>
也就是说,编译器自动实现了

大致的结构(由编译器自动生成)

private sealed class FooIterator : IEnumerable<int>, IEnumerator<int>
{
    private int _state;
    private int _current;

    public bool MoveNext()
    {
        switch (_state)
        {
            case 0:
                _state = 1;
                _current = 1;
                return true;

            case 1:
                _state = 2;
                _current = 2;
                return true;
            
            case 2:
                _state = 3;
                _current = 3;
                return true;
        }

        return false;
    }

    public int Current => _current;
}

就像Unity的Coroutine也是用C#的状态机做出来的一样

写一行yield return,实际底层是编译器在创建一个小型“协程”

执行流程

当写下

foreach (var x in Foo())
{
    ...
}

流程是这样的

  1. 调用Foo(),返回的是一个“迭代器对象”(状态机实例)
  2. foreach调用它的MoveNext()
  3. 方法运行到第一个yield return —— 暂停!返回元素
  4. 下一次MoveNext(),从暂停位置继续执行
  5. 再次遇到yield return—— 再暂停
  6. 直到代码跑到结尾

这就是“能暂停的函数”;把普通函数变成可挂起/可恢复的流程流程

yield return != return

这完全是两个东西

这种能力是在C++里必须手写状态机、lambda或coroutine才能实现

示例

  1. 在循环中求值
static IEnumerable<int> GenerateEvenNumbers(int count)
{
    for (int i = 0; i < count; i++)
    {
        // 每次返回一个值
        yield return i * 2;
    }
}

// 使用
foreach (int even in GenerateEvenNumbers(5))
{
    Console.WriteLine(even); // 输出 0, 2, 4, 6, 8
}
  1. 使用yield break提前终止
static IEnumerable<string> GetMessageUntilStop(string[] message)
{
    foreach (string msg in message)
    {
        if (msg == "stop")
        {
            yield break; // 立即终止
        }
        yield return msg;
    }
}

yield return的存在意义与适用场景

有些数据没必要一次性构造全部
比如读大文件

IEnumerable<string> ReadLines(string path)
{
    using var r = new StreamReader(path);
    stirng line;
    while ((line = r.ReadLine()) != null)
    {
        yield return line;
    }
}

传统写法会

yield return则是

适用场景

IEnumerable<Action> BehaviorTreeNode()
{
    yield return Patrol();
    yield return Chase();
    yield return Attack();
}

注意事项和常见误区

  1. 多次迭代 每次遍历迭代器方法,都会创建一个新的状态机实例,导致方法从头开始执行
var numvers = GetNumbers(); // 这只是一个IEnumerable<int> ,不是具体的数据

foreach (var n in numbers) { ... } // 执行一次 GetNumbers 方法
foreach (var n in numbers) { ... } // 再次执行 GetNumbers 方法,从头开始

如果方法内部有昂贵的操作(如数据库查询),这会导致性能问题。解决方法:如果确定需要多次遍历,可以将其具体化,例如通过.ToList().ToArray()

var numbersList = GetNumbers().ToList(); // 立即执行方法,将所有结果存入List
  1. 不支持所有接口 返回值类型必须是前面提到的四种接口之一

  2. IDisposable 编译器生成的状态机实现了IDisposable。如果使用手动枚举器,最好使用using语句

using (var enumerator = GetNumbers().GetEnumerator())
{
    while(enumerator.MoveNext())
    {
        ...
    }
}

(在foreach中,编译器会自动处理这些)

  1. 线程安全 编译器生成的状态机不是线程安全的。多个线程同时遍历同一个迭代器实例会导致不可预知的行为

[]

C#中的下标访问只是语法糖

something[index]

会被编译成

something.get_Item(index)

写入

something[index] = value;

等价于

something.set_Item(index, value);

这件事依赖于两个条件

  1. 类型具有名为Item的成员(通常是索引器)
  2. 类型标记了[DefaultMember("Item")]

只要满足这两点,C#编译器才能识别这个类型有“默认索引器”

[DefaultMember("Item")]

[DefaultMember("Item")]是个很隐蔽但很关键的老属性,它的作用是告诉编译器:这个类型的“默认成员”叫Item
换句话说,它让C#支持

obj[x]

这种下标访问语法
没有这个属性,编译器不知道[]应该绑定哪个成员函数\

Item

Item是索引器的真实名字,当你使用索引器时,其实在访问Item
C#把Item包装成一个“类数组的语法糖”:obj[x]本质是obj.Item[x]的语法糖
.NET BCL选择了一个通用名字Item,确保所有实现索引器的类型都能用同一套编译规则
这是设计者的取舍:强制统一名字,才能在语言层保证索引器语法是固定的\

存在意义

一切都来自CLR的世界比较古早的设计
在.NET初期,CLR并不知道C#所谓的“索引器”概念——在IL层面,它只是一个名字叫Item的属性
为了让C#编译器在看到obj[]语法后知道去找“Item属性”,就得告诉它:这个类的默认成员就是Item,于是有了这个属性

在现代C#中

因为现代C#语法糖越来越高级,实际写索引器时是这样

public class MyList<T>
{
    public T this[int index] => ...;
}

编译器自动生成

[DefaultMember("Item")]
public class MyList<T> { ... }

没有写它,编译器也会自动补充上
只有在

时才会看到它

XML注释

XML注释(也称为文档注释)在C#中是用来生成程序文档的工具,格式类似于XML,用于为类、方法、属性、字段等成员提供详细的说明。它不仅有助于代码的可读性,还能够通过工具生成API文档,方便其他开发者理解你的代码

基本结构

XML注释的基本格式如下

/// <summary>
/// 这是对成员的简短描述
/// </summary>

这种注释格式通常出现在方法、属性、类等成员的上方。<summary>标签用于提供简短的描述,描述该成员的作用

常见的XML注释标签

/// <summary>
/// 用户管理类
/// </summary>
/// <param name="id">
/// 用户ID
/// </param>
/// <returns>
/// 操作是否成功
/// </returns>
/// <remarks>
/// 这个方法线程安全
/// </remarks>
/// <exception cref="ArgumentNullException">
/// 参数为null时抛出
/// </exception>
/// <example>
/// 见代码示例
/// </example>

示例

  1. <remarks>用于补充详细信息,通常在<summary>之后
/// <summary>
/// 计算两个数的商
/// </summary>
/// <param name="a">被除数</param>
/// <param name="b">除数</param>
/// <returns>返回值</returns>
/// <remarks>如果除数为0,将抛出除零异常</remarks>
public double Divide(double a, double b)
{
    if (b == 0)
        throw new DivideByZeroException();
    return a / b;
}
  1. <exception>用于描述方法可能抛出的异常
/// <summary>
/// 从文件中读取数据
/// </summary>
/// <param name="filePath">文件路径</param>
/// <returns>返回文件内容</returns>
/// <exception cref="FileNotFoundException">文件未找到时抛出</exception>
/// <exception cref="IOException">读取过程中发生 IO 错误时抛出</exception>
public string ReadFile(string filePath)
{
    if (!File.Exist(filePath))
        throw new FileNotFoundException("文件未找到", filePath);

    return File.ReadAllText(filePath);
}
  1. 一个完整示例
/// <summary>
/// 表示一个用户实体
/// </summary>
/// <remarks>
/// <para>这个类用于存储用户的基本信息</para>
/// <para>包含用户ID、姓名和年龄</para>
/// </remarks>
public class User
{
    /// <summary>
    /// 用户ID
    /// </summary>
    /// <value>正整数,唯一标识用户</value>
    public int Id { get; set; }

    /// <summary>
    /// 用户姓名
    /// </summary>
    public string Name { get; set; }

    /// <summary>
    /// 根据ID查找用户
    /// </summary>
    /// <param name="id">要查找的用户ID</param>
    /// <returns>找到的用户对象,未找到时返回null</returns>
    /// <exception cref="ArgumentException">当id小于等于0时抛出</exception>
    /// <example>
    /// <code>
    /// var user = User.FindById(1);
    /// if (user != null)
    /// {
    ///     Console.WriteLine(user.Name);
    /// }
    /// </code>
    /// </example>
    /// <seealso cref="GetAllUsers"/>
    public static User FindById(int id)
    {
        if (id <= 0)
            throw new ArgumentException("ID必须大于0", nameof(id));

        // 实际查找逻辑
        return null;
    }

    /// <summary>
    /// 获取所有用户
    /// </summary>
    /// <typeparam name="T">返回的集合类型</typeparam>
    /// <returns>用户列表</returns>
    public List<User> GetAllUsers<T>() where T : class
    {
        return new List<User>();
    }
}
  1. 引用其他成员
/// <summary>
/// 使用<see cref="CalculateTotal"/>方法计算结果
/// 参考<seealse cref="MathHelper"/>
/// </summary>
  1. 条件注释
/// <summary>
/// 异步获取数据
/// </summary>
/// <include file='ExtraComments.xml' path='docs/members[@name="AsyncMethod"]/*'>

IDE支持和智能感知

注意事项

  1. XML必须格式良好:标签必须正确闭合
  2. cref属性值必须有效:引用的类型必须存在
  3. 注释与实际代码一致:避免误导
  4. 不要过度注释:自解释的代码不需要过多注释

使用XML的好处

  1. 自动化文档生成:通过XML注释,可以使用工具如Doxygen或Visual Studio的文档生成工具来自动生成API文档

    1. 项目配置 在.csproj文件中添加
      <PropertyGroup>
          <GenerateDocumentationFile>true</GenerateDocumentationFile>
          <DocumentationFile>bin\Debug\net8.0\MyProject.xml</DocumentationFile>
      </PropertyGroup>
      
    2. 编译输出 编译后生成XML文档文件,可以与以下工具配合
      • Sandcastle:生成CHM/网站文档
      • DocFX:生成现代Web文档
      • Swagger:API文档(配合Swashbuckle)
  2. IDE支持:许多IDE(如Visual Studio)能够解析这些注释,提供代码提示和文档预览,增强开发体验

    • 智能感知提示:鼠标悬停时显示XML注释
    • 快速文档视图:Ctrl+Q查看完整文档
    • 自动生成:输入///在方法/类上方自动生成模板
      /// <summary>
      /// 
      /// </summary>
      /// <param name="param1"></param>
      /// <returns></returns>
      
  3. 提升代码可读性:XML注释可以帮助开发者更清晰地理解代码的意图和用法,特别是在大型项目中,文档化的代码易于维护

  4. 标准化:XML注释使得代码文档化过程更规范,便于团队协作,确保文档内容完整