>> >> >> Reference << << << <<<<<<Ref>>>>>>
>> >> >> Indexer << << << <<<<<<Idx>>>>>>
Matched: 0

Tags

    Categories

      Types

        Top Results

          Miscellaneous
          M: 2025-06-01 - 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())
          {
              // 异步操作
          }
          

          适用场景

          • 文件 I/O 类(StreamReader,StreamWriter,FileStream
          • 数据库相关(SqlConnection, SqlCommand
          • 网络相关(HttpClient 某些用法)
          • 图形相关(Bitmap, Graphics
          • 其他实现了 IDisposable 的资源

          default(T)

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

          默认值规则

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

          1. 对于所有引用类型(classinterfacedelegatearraystring等)
          • default(T)返回null
          • 这表示一个“空”引用,不指向任何对象
          1. 对于值类型(struct和所有数字类型、boolcharenum等)
          • default(T)返回一个所有字段都被设置为各自默认值得实例(即“归零”的实例)
          • 这通常意味着
            • 数值类型(int, double, decimal等)返回0
            • bool返回false
            • char返回'\0'(空字符)
            • enmu返回(E)0,即使这个枚举值可能没有定义
            • 自定义的struct返回一个实例,其中每个字段都被设置为它的默认值

          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}";
          
          • =>替换了{ return ...; }
          • 表达式的结果自动作为方法的返回值
          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等)
          • 可行:=> x + y(表达式)
          • 不可行:=> { if (x > y) return x; else return y; }(语句块)
          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#本质上是一种静态类型语言

          • 静态类型(如int, string, MyClass):在编译时,编译器就知道变量的类型。任何不符合类型的操作(比如对一个string变量进行数学运算)都会导致编译错误。这提供了安全性、智能感知(IntelliSense)和性能优化
          • 动态类型(dynamic):编译器对dynamic变量“放手不管”。它假设你在运行时对该变量做的任何操作都是有效的。编译时不会进行类型检查,也没有智能感知。所有的类型解析、方法调用、属性访问等都在程序运行时动态进行

          基本用法和语法

          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方法,运行时这里会抛出异常
          

          优缺点

          优点

          • 灵活性:可以编写非常灵活、适应多种类型的代码
          • 简化代码:在与COM或动态语言交互时,代码变得极其简洁
          • 开发效率:在快速原型开发或处理不确定数据结构时,可以提高效率

          缺点

          • 性能开销:运行时解析类型和方法调用比直接的静态调用慢,因为涉及反射等机制
          • 失去编译时安全:这是最大的风险。如果代码中有拼写错误或类型不匹配,编译器不会报错,直到运行时才会抛出RuntimeBinderException
          • 失去智能感知:在Visual Studio中,对dynamic变量不会有自动完成、方法列表等提示,这会影响开发体验和代码可读性

          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

          • when是一个修饰条件的“过滤器”。它的核心作用是“进一步限定匹配的条件”
          • where用于泛型约束,意思是“这个泛型类型或方法,只接受满足某种条件的类型”

          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

          • 普通class比较的是“引用是否相同”
          • record比较的是“字段内容是否相同”

          这也是它被称为“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)
          
          • record class:引用类型,按值比较
          • record struct:值类型,按字段比较

          解构(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; }
          }
          

          底层原理(编译后)

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

          • 构造函数
          • EqualsGetHashCode
          • ToString
          • Deconstruct
          • with克隆支持

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

          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}");
          
          • int n是一个类型模式
          • > 0是一个关系模式
          • and把它们组合成“是int且大于0”的模式
          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
          
          • 类型检查:is可以检查对象是否是指定类型,或者是否可以隐式转换为目标类型
          • 返回值:如果对象符合目标类型,则返回true,否则返回false

          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“
          
          • as用于将对象转换为目标类型,并且如果转换失败返回null,这对引用类型非常有用
          • 无法转换的情况下,as返回null(而不是抛出异常)

          对于值类型,如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. 类型检查与转换
          • is用来检查对象是否是某个类型的实例,并且可以在类型匹配时进行转换
          • as用来尝试将对象转换为指定类型,如果转换失败,返回null
          1. 适用场景
          • is更适用于你只关心类型是否匹配的场景
          • as更适用于你不确定类型转换是否会成功的场景,避免异常抛出
          1. 性能差异
          • is在检查类型时,可能需要更复杂的检查(特别时类型匹配时)
          • as通常比is性能好,因为它直接尝试转换,而不会检查类型后转换

          使用场景示例

          场景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;
          }
          
          • return会立刻离开当前函数
          • 常用于错误检测、提前退出或返回结果

          yield return

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

          • 目的:能轻松编写一个方法,该方法可以按需返回一个序列(如集合)中的元素,而不是一次性返回整个集合
          • 关键字:yield return, yield break
          • 返回值类型:使用yield return的方法的返回值必须是IEnumerable, IEnumerable<T>IEnumeratorIEnumerator<T>

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

          执行模型

          当写一个含有yield return的方法

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

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

          • 一个状态机类
          • 一个记录当前运行进度的字段(如state = 0, 1, 2…)
          • MoveNext()方法中用switch(state)控制流程
          • Current()属性返回当前值

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

          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

          这完全是两个东西

          • return 方法结束
          • yield 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则是

          • 读到一行 -> 立刻往外产出
          • 外部消费一行 -> 继续读下一行
          • 内存占用极小

          适用场景

          • 惰性求值 这是yield return最重要得特性。元素只在被请求时(即foreach循环到它时)才生成。这在处理大量数据或无限序列时极其高效,因为它不需要一次性将所有数据加载到内存中
          • 大数据流处理
          • 图遍历(DFS/BFS)
          • 编写解析器(Yield-based tokenizer)
          • 组合复杂迭代逻辑(比如Unity的协程) 比如根据角色状态生成下一帧行为
          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> { ... }
          

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

          • 用反射
          • 自己生成IL
          • 做CodeDOM
          • 写动态类型桥接层
          • 分析第三方编译器产生的代码
          • 搞C#和其他语言互操作(比如COM或IronPython)

          时才会看到它

          XML注释

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

          基本结构

          XML注释的基本格式如下

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

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

          常见的XML注释标签

          • <summary>:简要说明一个成员的作用或功能
          /// <summary>
          /// 用户管理类
          /// </summary>
          
          • <param>:描述方法参数,通常用于方法和构造函数。name属性指定参数的名称
          /// <param name="id">
          /// 用户ID
          /// </param>
          
          • <returns>:说明方法返回值的含义
          /// <returns>
          /// 操作是否成功
          /// </returns>
          
          • <remarks>:提供额外的说明信息,通常用于描述实现细节
          /// <remarks>
          /// 这个方法线程安全
          /// </remarks>
          
          • <exception>:描述方法可能抛出异常
          /// <exception cref="ArgumentNullException">
          /// 参数为null时抛出
          /// </exception>
          
          • <example>:给出如何使用该方法、类或属性的示例
          /// <example>
          /// 见代码示例
          /// </example>
          
          • <value>:用于描述属性的值
          • <typeparam>:泛型类型参数说明
          • <seealso>:参考其他相关主题
          • <code>:内嵌代码
          • <para>:段落格式(在<remarks>中使用)

          示例

          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注释使得代码文档化过程更规范,便于团队协作,确保文档内容完整