Skip to content

深入理解CSharp

约 2262 字大约 8 分钟

笔记NET

2026-01-21

进化史

C#1.0

  • 面对对象基本特性:类、接口、继承、多态
  • 值类型与引用类型
  • 只读属性
  • 弱类型集合
  • 委托

C#2.0

  • 泛型
  • 强类型集合
  • 可为空类型 int?
  • 迭代器 yield return/yield break
  • 外部别名

C#3.0

  • 自动属性、简单初始化 public string Name{get;set;}
  • 隐式类型 var
  • 扩展方法
  • Lambda表达式
  • Linq (Language Integrated Query)

C#4.0

  • 动态类型(dynamic)
  • 命名实参、可选参数 Funct(Name:"123",Age:1);

C#5.0

  • 异步函数 await/async

委托 delegate

类似函数指针

委托提供了简洁方法,不需要直接指定一个需要执行的行为,而是将这个行为用某种方式包含在一个对象中,这个对象可以向其他对象那样使用。

可以将委托看成一个定义了一个方法的接口,委托的实例就是在实现那个接口

构成

  • 声明委托类型 delegate void StringProcessor(string input);
  • 委托实例方法 具有和委托类型相同的函数 (参数、返回值)
  • 创建委托
StringProcessor proc1;
proc1 = new StringProcessor(PrintStr);
  • 调用委托 proc1("aa") -> proc1.invoke("aa")//编译后的 -> PrintStr("aa")//实际执行

值类型引用类型

值类型:

  • 枚举
  • 结构 引用类型:
  • 字符串
  • 数组
  • 委托
  • 接口

外部别名

当同一个程序集中引用了两个(或多个)版本完全相同的命名空间 + 相同的类型名,但它们来自不同的程序集时可以使用

假设项目里同时引用了:

  • Newtonsoft.Json 12.0.3(旧版)
  • Newtonsoft.Json 13.0.3(新版) 两个版本里都有 Newtonsoft.Json.JsonConvert 这个类。 普通写法会直接冲突,编译报错。

解决办法:使用 extern alias

  1. 在项目文件(.csproj)里给两个引用起别名
<ItemGroup>
  <!-- 旧版 -->
  <Reference Include="Newtonsoft.Json">
    <HintPath>..\lib\Newtonsoft.Json.12.0.3.dll</HintPath>
    <Aliases>Json12</Aliases>
  </Reference>

  <!-- 新版 -->
  <PackageReference Include="Newtonsoft.Json" Version="13.0.3">
    <Aliases>Json13</Aliases>
  </PackageReference>
</ItemGroup>

(如果是 .NET SDK 风格项目,PackageReference 也可以加 Aliases 属性)

  1. 在代码文件顶部声明外部别名
extern alias Json12;
extern alias Json13;

using Json12::Newtonsoft.Json;
using Json13::Newtonsoft.Json;

// 现在可以明确区分
var oldSerializer = new Json12::JsonSerializer();   // 用 12.0.3 版本
var newSerializer = new Json13::JsonSerializer();   // 用 13.0.3 版本

string oldWay = Json12::JsonConvert.SerializeObject(obj);
string newWay = Json13::JsonConvert.SerializeObject(obj);

泛型约束

确保只接受指定的引用类型或值类型

引用类型约束

实参是引用类型,必须是类型参数的第一个约束 用T : class表示

sturct RefSample<T> where T : class

示例:

  • RefSample<IDisposable>
  • RefSample<string>
  • RefSample<int>

反例

  • RefSample<Guid>
  • RefSample<int>

值类型约束

确保使用的类型实参是值类型,包括枚举 T : struct

class ValSample<T> where T : sturct

示例:

  • ValSample<int>

反例

  • ValSample<object>

构造函数类型约束

检查类型实参是否有一个可以用于创建类型示例的无参构造函数 适用于所有值类型 必须是所有类型的最后一个约束 T : new()

  • class Sample<T> where T : new() //T必须有公共的无参构造函数

转换类型约束

约束为你指定的一个类型

  • class Sample<T> where T : Stream //T必须是Stream类或Stream的子类
  • class Sample<T> where T : U //T必须是U的子类或实现类
  • class Sample<T> where T : IComparable<T> //T必须实现IComparable接口

组合约束

就是上面的约束组合使用

有效:

  • class Sample<T> where T: class, IDisposable, new()
  • class Sample<T> where T: struct, IDisposable
  • class Sample<T,U> where T: class where U: struct, Tclass Sample<T,U> where T: Stream where U : IDisposable

无效:

  • class Sample<T> where T : class, struct
  • class Sample<T> where T: Stream, class
  • class Sample<T> where T : new(), Stream
  • class Sample<T> where T: IDisposable, Stream
  • class Sample<T> where T:XmlReader, Icomparable, Icomparable
  • class Sample<T,U> where T: struct where U: class, T
  • class Sample<T,U> where T: Stream, U: IDisposable

迭代器

迭代器是一种行为模式的范例。
行为模式是一种简化对象之间通讯的涉及模式,这是一种非常易于理解和使用的模式。它允许你访问一个数据集合中的所有元素,不用关心数据类型。能非常有效的构建出一个数据管道,经过一些列不同的转换过滤后从管道的另一头出来。
这也是Linq的核心模式之一

迭代器主要通过IEnumeratorIEnumerable接口来封装的。 如果某个类型实现了IEnumerable 就意味着它可以被迭代访问 调用GetEnumerator方法将放回IEnumerator的实现,就是迭代器本身。

使用嵌套类实现集合迭代器:

public class MyNumbers : IEnumerable<int>
{
    private readonly int[] _data = { 10, 20, 30, 40, 50 };
    public IEnumerator<int> GetEnumerator()
    {
        return new MyNumbersEnumerator(this);
    }
    IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();

    // 嵌套的枚举器类(状态机)
    private class MyNumbersEnumerator : IEnumerator<int>
    {
        private readonly MyNumbers _parent;
        private int _index = -1;   // 注意:通常从 -1 开始!

        public MyNumbersEnumerator(MyNumbers parent)
        {
            _parent = parent;
        }
        public int Current
        {
            get
            {
                if (_index < 0 || _index >= _parent._data.Length)
                    throw new InvalidOperationException("枚举器位置无效");
                return _parent._data[_index];
            }
        }
        object IEnumerator.Current => Current;
        public bool MoveNext()
        {
            _index++;
            return _index < _parent._data.Length;
        }
        public void Reset()
        {
            _index = -1;   // 或者 throw new NotSupportedException();
        }
        public void Dispose()
        {
            // 如果有需要释放的资源(如文件、数据库连接)放这里
        }
    }
}

使用 yield return 简化迭代器

如果使用yield return来实现:

public IEnumerator GetEnumerator()
{
	for(int index=0;index < values.Length; index++)
	{
		yield return values[(index + startingPoint) % values.Length];
	}
}

原理: 看似写了一个顺序执行的函数,但是实际上是请求编译器创建了一个状态机;编译器看到迭代块时,会为状态机创建一个嵌套类型,来记录块中的位置以及局部变量包括参数的值。

在常规函数中,return具有两个作用:返回值与中止函数的执行并且在退出时执行finally

yield return语句具有临时退出函数的作用,直到再次调用MoveNext后又继续执行,在其中好像没有检查finally代码块的行为?实际发生了些什么

  • 正常来说在离开相关作用域时,会执行finally代码块。 迭代器和普通函数不一样yield return是暂停函数,并没有退出所以不会执行finally。(结合yield break能做到)

yield return 应用:

//简单的读取文本文件的函数
static IEnumerable<string> ReadLines(Func<TextReader> provider)
{
	using(TextReader reader = provider())
	{
		string line;
		while((line = reader.ReadLine())!=null)
		{
			yield return line;
		}
	}
}

static IEnumerable<string> ReadLines(string filename) => return ReadLines(filename,Encode.UTF8);

static IEnumerable<string> ReadLines(string filename,Encoding encoding)
{
	return ReadLines(delegate {  return File.OpenText(filename, encoding);  })
}



//其中使用了泛型、匿名函数、迭代器块 
//做到了只有在需要的时候才去获取资源,随时处于IDisposable的上下文中,随时剋释放资源
//每次都会创建独立的RextReader 不会存在占用的问题

使用场景

// 场景1:只读前 10 行(非常省资源)
foreach (var line in ReadLines("very-large.log"))
{
    Console.WriteLine(line);
    if (++count >= 10) break;   // 中途 break,文件会立刻关闭
}

// 场景2:过滤 + 转换(不把整个文件读进内存)
var errorLines = ReadLines("app.log")
    .Where(line => line.Contains("ERROR"))
    .Take(100);

// 场景3:LINQ 链式操作
var users = ReadLines("users.csv", Encoding.UTF8)
    .Skip(1)                    // 跳过标题行
    .Select(line => line.Split(','))
    .Select(parts => new User(parts[0], parts[1]));

用 Func<TextReader> + using + yield return 三者组合,实现了:

  • 延迟打开文件(第一次迭代才打开)
  • 正确释放资源(即使中途 break 或异常)
  • 惰性读取(不把整个大文件一次性读进内存)
  • 接口友好(返回 IEnumerable<string>,可直接 foreach / LINQ) .NET 中处理大文件行读取的经典、优雅、安全写法之一,几乎所有现代的文件读取工具类(包括 .NET 源码中的一些 helper)都采用类似结构。

Linq

查询表达式

  • 序列
    • 执行表达式时 是先获取结果在返回时过滤或者其他处理
      • var result = from person in people
          		  where person.Age >= 18
          		  select person.Name
          //调用者 -> select -> where -> list(people) -> where(开始过滤Age>=18) -> select(Name) -> 拿到结果

提示

从表达式的写法就能看出来他的执行顺序 from ... where ... select,而不是像sql一样。因为是从数据源开始,where过滤,select投影

  • 延迟查询

    • 查询表达式被创建时,不会处理任何数据,而是在内存中生成这个查询的表现形式,直到访问结果的元素时 才开始使用
  • let子句

    • 简单的理解为中间变量

Lambda

匿名函数演化:

  1. 转换为Lambda表达式

    delegate(String text) { return text.Length; }
  2. 单个表达式,无括号

    (String text) => { return text.Length; }
  3. 让编译器推断参数类型

    (String text) => text.Length
  4. 去除不必要的括号

     text => text.Length

表达式树

// 手动构建 x => x + 3 * 2
var parameter = Expression.Parameter(typeof(int), "x");

var constant3 = Expression.Constant(3);
var constant2 = Expression.Constant(2);

var multiply = Expression.Multiply(constant3, constant2);   // 3*2
var add = Expression.Add(parameter, multiply);              // x + (3*2)

var lambda = Expression.Lambda<Func<int, int>>(add, parameter);

var func = lambda.Compile();
Console.WriteLine(func(10));   // 输出 16

扩展方法

特征

  • 在非嵌套、非泛型的静态类(所以必须是静态方法)
  • 至少要有一个参数
  • 第一个参数必须附加this关键字
  • 第一个参数不能有任何其他修饰符 ref\out之类的
  • 第一个参数不能是指针类型