第2章 C#2

第2章 C#2

2.1 泛型

2.1.1 示例:泛型诞生前的集合

在泛型诞生之前(.NET1),开发者常用如下方式创建集合:

  • 数组

  • 普通对象集合

    ArrayList​、Hashtable

  • 专用类型集合

    StringCollection

// 数组
static string[] GenerateNames()
{string[] names = new string[4];names[0] = "Gamma";names[1] = "Vlissides";names[2] = "Johnson";names[3] = "Helm";return names;
}
static void PrintNames(string[] names)
{foreach (string name in names){Console.WriteLine(name);}
}
// ArrayList
static ArrayList GenerateNames()
{ArrayList names = new ArrayList();names.Add("Gamma");names.Add("Vlissides");names.Add("Johnson");names.Add("Helm");return names;
}
static void PrintNames(ArrayList names)
{foreach (string name in names){Console.WriteLine(name);}
}
// StringCollection
static StringCollection GenerateNames()
{StringCollection names = new StringCollection();names.Add("Gamma");names.Add("Vlissides");names.Add("Johnson");names.Add("Helm");return names;
}
static void PrintNames(StringCollection names)
{foreach (string name in names){Console.WriteLine(name);}
}

在只需要处理 string​ 类型的情况下,StringCollection​ 是不二之选。如果需求其他类型集合,而 .NET Framework 又未实现,那只能自己写一个了。为此 .NET 提供了 System.Collections.CollectionBase ​ 抽象类,减少重复工作。

2.1.2 泛型降临

泛型的出现解决了 C#1 中集合无法灵活定义的问题。上节的例子改用 List<T>​ 后如下:

static List<string> GenerateNames()
{List<string> names = new List<string>();names.Add("Gamma");names.Add("Vlissides");names.Add("Johnson");names.Add("Helm");return names;
}
static void PrintNames(List<string> names)
{foreach (string name in names){Console.WriteLine(name);}
}

2.1.2.1 类型形参与类型实参

形参(parameter)和实参(argument)的概念很早便出现了:

image

相对的,泛型也引入了两个参数概念:** 类型形参 (type parameter)和 类型实参 **(type argument):

image

2.1.2.2 泛型类型和泛型方法的度

泛型度(arity)是泛型声明中类型形参的 数量 。我们可将非泛型的声明视为泛型度为 0

泛型度是区分同名泛型声明的有效指标。以如下声明为例,这三个方法的泛型度各不相同:

深入解析C(第4版).pdf - p49 - 深入解析C(第4版)-P49-20250103160834-4zfnnwn

需要注意的是,多个类型形参 不能 采用相同的名字(类似于不同参数不能同名)。以下声明是 非法 的:

public void Method<T, T>() {}

2.1.3 泛型的使用范围

对于类型,泛型适用于:

类型 是否适用
枚举
结构体
接口
委托

对于成员,泛型仅适用于 方法 。以下类型成员不能是泛型:

  • 字段
  • 属性
  • 索引器
  • 构造器
  • 事件
  • 终结器

有些类型成员适用了其他泛型类型,看似是泛型成员,实则 不是 。只需记住一条原则:判断一个声明是否是泛型声明的唯一标准,是看它是否 引入了新的类型形参

以如下代码为例,items 实际 不是 泛型成员:

public class ValidatingList<TItem>
{private readonly List<TItem> items = new List<TItem>();
}

2.1.4 方法类型实参的类型推断

对于泛型方法,编译器可以隐式推断出类型实参。以如下代码为例,二者都能正常编译:

public static List<T> CopyAtMost<T>(List<T> input, int maxElements) { ... }List<int> numbers = new List<int>();
...
List<int> firstTwo = CopyAtMost<int>(numbers, 2);
public static List<T> CopyAtMost<T>(List<T> input, int maxElements) { ... }List<int> numbers = new List<int>();
...
List<int> firstTwo = CopyAtMost(numbers, 2);

需要注意,编译器只能推断出传递给方法的类型实参,但推断不出 返回值 的类型实参。

类型推断在简化泛型类型实例的创建方面很有帮助。以 .NET4.0 的元组为例,其 Create()​ 工厂方法通过隐式推断获取实例更为简洁:

public static Tuple<T1> Create<T1>(T1 item1)
{return new Tuple<T1>(item1);
}
public static Tuple<T1, T2> Create<T1, T2>(T1 item1, T2 item2)
{return new Tuple<T1, T2>(item1, item2);
}

试比较如下两段代码,显然使用工厂方法更为简洁(泛型类型推断不适用于构造器):

new Tuple<int, string, int>(10, "x", 20)
Tuple.Create(10, "x", 20)

如果推断的类型不是我们想要的类型,我们可以通过 显式类型转换 进行限制,或显式指定类型实参:

// 目标类型为 Tuple<int, object, int>
Tuple.Create<int, object, int>(10, "x", 20);
// or
Tuple.Create(10, (object) "x", 20)
// 目标类型为 Tuple<string, int>
Tuple.Create((string) null, 50)

2.1.5 类型约束

泛型约束可以:

  • 约束方法实参的值的 类型
  • 约束方法内部如何 操作、使用 T​ 类型的值

以如下代码为例,它限制 T​ 必须实现 IFormattable ​ 接口

static void PrintItems<T>(List<T> items) where T : IFormattable
{CultureInfo culture = CultureInfo.InvariantCulture;foreach (T item in items){Console.WriteLine(item.ToString(null, culture));}
}
类型约束适用的范围

类型约束适用于:

接口约束

语法为: where T : SomeInterface

引用类型约束

语法为: where T : class

class 关键字表示任何引用类型,包括接口、委托。

值类型约束

语法为: where T : struct

类型实参必须是非可空类型(结构体或枚举),可空类型不适用于本约束。

构造器约束

语法为: where T : new()

转换约束

语法为: where T : SomeType

此处 SomeType​ 可以是 接口 或其他 类型形参

  • where T : Control
  • where T : IFormattable
  • where T1 : T2

类型约束 可以 组合使用,组合规则比较复杂,此处不再赘诉。如果违法了相关规则,编译器会给出明确的错误信息。

多个类型形参的泛型约束

一个声明存在多个类型形参时,每个类型形参都可以有各自的类型约束:

TResult Method<TArg, TResult>(TArg input)where TArg : IComparable<TArg>where TResult : class, new()

2.1.6 default 运算符和 typeof 运算符

2.1.6.1 default 运算符

default 运算符是一元运算符,返回传入类型的默认值。引用类型默认值为 null ;非可空值类型,返回对应类型的“ 0 值”(0、0.0、0.0n、false、UTF-16 编码的单元 0 等);可空值类型,返回 该类型的 null 值

default 用于泛型类型。下面几种形式均合法:

  • default(T)
  • default(int)
  • default(string)
  • default(List<T>)
  • default(List<List<string>>)

2.1.6.2 typeof 运算符

typeof 运算符的使用相对复杂,可以分为 5 种情况(如下例子假设 T​ 实际传入的是 double​ 类型):

情形 例子 返回内容
不涉及泛型类型 typeof(string) String
涉及泛型类型,不涉及类型形参 typeof(List<int>) List<Int32>
仅涉及类型形参 typeof(T) Double
涉及泛型,且泛型作为类型形参 typeof(List<TItem>) List<Double>
涉及泛型,但操作数中不含类型实参 typeof(List<>) List<T>

上述例子中,List<>​ 的写法仅在 typeof​ 中是有效的。如果参数多于一个,每增加一个参数就 增加一个逗号 。如 Dictionary<TKey, TValue>​ 的泛型定义,要写成 typeof(Dictionary<,>)

2.1.7 泛型类型初始化与状态

对于泛型类型,每个封闭的、已构造类型都会被单独初始化,并且拥有各自的静态域

以如下代码为例,string​ 和 int​ 最终对应的计数值为 21 ,且静态构造函数执行了 2 次:

class GenericCounter<T>
{private static int value;static GenericCounter(){Console.WriteLine("为 {0} 类型初始化计数器", typeof(T));}public static void Increment(){value++;}public static void Display(){Console.WriteLine("{0} 类型的计数为: {1}", typeof(T), value);}
}
class GenericCounterDemo
{static void Main(){GenericCounter<string>.Increment();GenericCounter<string>.Increment();GenericCounter<string>.Display();GenericCounter<int>.Display();GenericCounter<int>.Increment();GenericCounter<int>.Display();}
}

如果泛型类型进一步嵌套,问题将更加复杂。以如下代码为例,右侧所示的 4 种类型都是 独立 的,各自拥有 value​ 字段。:

class Outer<TOuter>
{class Inner<TInner>{static int value;}
}
  • Outer<string>.Inner<string>
  • Outer<string>.Inner<int>
  • Outer<int>.Inner<string>
  • Outer<int>.Inner<int>

2.2 可空值类型

2.2.2 CLR 和 framework 的支持:Nullable<T>​ 结构体

可空值类型背后的核心要素是 Nullable<T>​ 结构体,该结构体的一个早期版本如下:

public struct Nullable<T> where T : struct
{private readonly T value;private readonly bool hasValue;public Nullable(T value){this.value = value;this.hasValue = true;}public bool HasValue { get { return hasValue; } }public T Value{get{if (!hasValue){throw new InvalidOperationException();}return value;}}
}

深入解析C(第4版).pdf - p60 - 深入解析C(第4版)-P60-20250106133511-nqgnlww

其中 where T : struct​ 约束 T 只能是除 Nullable<T> ​ 外的任意值类型。此外,Nullable<T>​ 还提供了如下这些方法和运算符:

  • GetValueOrDefault()​、GetValueOrDefault(T defaultValue)​ 方法

  • 重写了 object​ 类的 Equals(object) ​ 和 GetHashCode() ​ 方法

    其行为更加明确:先比较 HasValue​ 属性;当两个对象的 HasValue​ 均为 true ,再比较 Value ​ 属性是否相等。

  • 支持 T ​ 到 Nullable<T>​ 的隐式类型转换

  • 支持 Nullable<T>​ 到 T ​ 的显式类型转换

    HasValue​ 为 false 时则抛出 InvalidOperationException ​ 异常。该转换等同于使用 Value ​ 属性。

Eureka

我们与其说 where T : struct​ 是不允许 Nullable<T>​,倒不如说是不允许“可为 null 的类型”。如下代码编译器会报 编译器错误 CS0453 - C# | Microsoft Learn:

Nullable<Nullable<int>> value = new();

该限制仅对 Nullable<T>​ 有效(应该是编译器打了洞),我测试自定义的可空类型并不会有该错误:

MyNullable<MyNullable<int>> value = new();
Nullable<MyNullable<int>> value2 = new();public struct MyNullable<T> where T : struct
{private readonly T value;private readonly bool hasValue;public MyNullable(T value){this.value = value;this.hasValue = true;}public bool HasValue { get { return hasValue; } }public T Value{get{if (!hasValue){throw new InvalidOperationException();}return value;}}
}

2.2.2.1 装箱行为

普通值类型(这里指非可空值类型)和可空值类型面对装箱行为稍有不同。

  • 普通值类型:以 int​ 类型为例,装箱前后调用 GetType()​ 方法返回的结果和 typeof(int)相同

    // x.GetType() 和 o.GetType() 得到的类型相同。
    int x = 5;
    object o = x;
    
  • 可空值类型:没有对等的装箱类型。装箱结果视 HasValue​ 属性值决定:

    • false:装箱后为 null 引用
    • true:装箱后为 T对象的 引用
    Nullable<int> noValue = new Nullable<int>();
    object noValueBoxed = noValue;
    Console.WriteLine(noValueBoxed == null);
    Console.WriteLine(noValueBoxed.GetType());  // 抛出 NullReferenceExceptionNullable<int> someValue = new Nullable<int>(5);
    object someValueBoxed = someValue;
    Console.WriteLine(someValueBoxed.GetType());
    

    深入解析C(第4版).pdf - p62 - 深入解析C(第4版)-P62-20250106155158-8bf8rws

Tips

值类型调用 GetType()​ 方法有一个副作用:总会触发一次 装箱操作 。这是因为 object.GetType()​ 方法是非虚方法(不能重写)。

该行为多少会影响效率(但不至于造成困扰)。

2.2.3 语言层面支持

2.2.3.1 ? ​ 后缀

C# 为 Nullable<T>​ 提供了一个简化版写法,即在类型名后添加 ? ​ 后缀。下面 4 个声明完全等价:

  • Nullable<int> x;
  • Nullable<Int32> x;
  • int? x;
  • Int32? x;

2.2.3.2 null 字面量

C#1 中 null 表达式永远代指一个 null 引用 。C#2 因引入了可空类型,null 的含义发生了扩展:

  • 表示一个 null 引用
  • 或者表示一个 HasValue​ 为 false可空类型 的值。

null 引用和可空值类型不容易辨明。如下两行代码 等价的(第 种更常用):

int? x = new int?();
int? x = null;

我们判断可空类型是否有值可用如下两种方式(根据编码习惯选择,没有优劣):

if (x != null)
if (x.HasValue)

2.2.3.3 转换

前面我们提到 T​ 和 Nullable<T>​ 之间有隐式/显式转换。此外,C# 还允许链式转换:对于任意两个非空的值类型 S​ 和 T​,如果存在从 S​ 到 T​ 的类型转换(如 int​ 转为 decimal​),则以下类型转换都是合法的:

  • Nullable<S>​ 到 Nullable<T>​ 的类型转换( 显式转换或隐 式转换,视 S 到 T 的转换类型而定);
  • S​ 到 Nullable<T>​ 的类型转换(同上);
  • Nullable<S>​ 到 T​ 的 式类型转换。

上述转换的原理是:将 S 到 T 按照要求进行转换,并为其填充空值。这种操作被称为** 提升 lifting**)

Notice

无论是可控值还是非可空值,都可以进行显式类型转换。LINQ to XML 很好的利用了该特性。见10.6.2.1 XML 对象与空运算符

2.2.3.4 提升运算符

C# 允许对如下运算符进行重载:

一元运算符 + - ! ~ ++ --
四则运算 + - * / %
位运算 & | ^ << >>
比较 == != > < >= <=

深入解析C(第4版).pdf - p64 - 深入解析C(第4版)-P64-20250106175336-fep62gz

Nullable<T>​ 重载了 T​ 所重载的上述运算符。不过在可空类型中,这些运算符的操作数类型、返回值类型与非可空类型有所区别,因此被称为“** 提升 运算符**”。提升运算符要遵循如下规则:

  • true 和 false 运算符 不能 被提升;

    但二者很少用,因此影响不大。

    Info

    关于 true、false 运算符,见4.14.4 重载 true 和 false

  • 只有操作数是 非可空值 类型的运算符才能被提升;

  • 对于一元运算符和二元运算符(即四则运算和位运算),原运算符的返回类型必须是 非可空的值 类型;

  • 对于等价运算符和关系运算符(即比较运算),原运算符的返回类型必须是 bool ​ 类型;

  • 作用于 Nullable<bool>​ 的 &​ 和 |​ 运算符具有单独定义的行为,稍后介绍。

上述运算符的运算遵循如下规则:

  • 任意一个 操作数为 null,则返回值也为 null;
  • 等价运算符和关系运算符(即比较运算)返回值是 非可 空的布尔型;
  • 等价运算,两个 null 被视为 等,一个 null 和一个非 null 则 等;
  • 关系运算符,任意一个操作数为 null 时,总是返回 false
22.3.4.1 向可空整数应用提升运算符的例子

下面用 int​ 来举例说明。我们假定有 3 个变量:four​、five​ 和 nullInt​,它们都是 Nullable<int>​ 类型,值分别是 4、5、null:

表达式 提升运算符 结果
-nullInt int? -(int? x) null
-five int? -(int? x) -5
five + nullInt int? +(int? x, int? y) null
five + five int? +(int? x, int? y) 10
four & nullInt int? &(int? x, int? y) null
four & five int? &(int? x, int? y) 4
nullInt == nullInt bool ==(int? x, int? y) true
five == five bool ==(int? x, int? y) true
five == nullInt bool ==(int? x, int? y) false
five == four bool ==(int? x, int? y) false
four < five bool <(int? x, int? y) true
nullInt < five bool <(int? x, int? y) false
five < nullIn bool <(int? x, int? y) false
nullInt < nullInt bool <(int? x, int? y) false
nullInt <= nullInt bool <=(int? x, int? y) false

2.2.3.5 可空逻辑

下表是 Nullable<bool>​ 的 4 个逻辑运算符的真值表。其中与(&)或(|)具有特殊行为,列表中额外规则都已加粗:

x y x & y x | y x ^ y !x
true true true true false false
true false false true true false
true null null ** true ** null false
false true false true true true
false false false false false true
false null
** false ** null null true
null true null ** true ** null null
null false ** false ** null null null
null null null null null null

深入解析C(第4版).pdf - p66 - 深入解析C(第4版)-P66-20250107103456-ansmroj

Warn

提升运算符的执行结果是 C#特有的

本节所讨论的提升运算符、类型转换以及 Nullable<bool>​ 逻辑等特性都是由 C#编译器提供的,而不是由 CLR 或 framework 本身提供的。如果使用 ildasm 工具检查上述可空值运算符的代码,就会发现是编译器创建了所有 IL 代码来进行空值检查,并做出相应处理。
因此,不同语言处理 null 值的方式会有所不同。如果需要在基于.NET 平台的不同语言之间移植代码,就需要格外小心了。例如 Visual Basic 中提升运算符的行为就更接近 SQL:当 x​ 或 y​ 为 null 时,x < y​ 的结果也为 null

2.2.3.6 as 运算符与可空值类型

自 C#2 开始,as 运算符可用于可空值类型(在此之前只能用于 引用 类型)。该运算符的返回值为一个可空类型的值:当原始引用的类型为 null 或与目标类型不匹配时,返回 null 值,或者返回一个有意义的值,示例如下:

static void PrintValueAsInt32(object o)
{int? nullable = o as int?;Console.WriteLine(nullable.HasValue ? nullable.Value.ToString() : "null");
}PrintValueAsInt32(5);                // 打印 5
PrintValueAsInt32("some string");    // 打印 null

2.2.3.7 空合并运算符 ??

空合并运算符(??​)用于解决这样一个问题:当一个表达式运算结果为 null 时,为变量提供一个 默认 值。

??​ 是一个二元运算符,first ?? second​ 表达式的计算步骤如下:

  1. 计算 first​ 表达式;
  2. 若结果不为 null ,则整个表达式的结果等于 first 的计算结果;
  3. 若结果为 ,则继续计算 second 表达式,整个表达式的结果为 second 的计算结果。

空合并运算符还能组合使用:

int? value = x ?? y ?? z;

Notice

如果第一个操作数的类型是可空值类型,第二个操作数是非可空值类型,整个表达式的类型将是 非可空值 类型!例如以下代码是合法的:

int? a = 5;
int b = 10;
int c = a ?? b;

2.3 简化委托的创建

2.3.1 方法组转换

方法组:即一个或多个同名方法。我们每次对方法的调用就是对方法组的一次使用。以如下代码为例,Console.WriteLine​ 就是一个方法组,编译器会根据方法的 调用实参 从方法组中选择合适的重载方法进行调用:

Console.WriteLine("hello");

方法组除了被调用,还可以用于委托创建表达式。在 C#1 中,其使用方式为:

private void HandleButtonClick(object sender, EventArgs e) { ... }
// 假设 EventHandler 签名为:public delegate void EventHandler(object sender, EventArgs e)
EventHandler handler = new EventHandler(HandleButtonClick);

C#2 通过方法组转换简化了这一操作:只要委托的签名与方法组中任何一个重载方法 兼容 ,该方法组就可以隐式地转换为该委托类型。上面的例子可以简化为:

EventHandler handler = HandleButtonClick;

这两种方式最终会生成同样的 IL 代码。

2.3.2 匿名方法

匿名方法:无须在创建委托实例前预先编写另一个实体方法(该方法最终还是会出现在 IL 代码中) ,只需在委托中创建内联代码即可。

匿名方法的大体使用步骤是:使用 delegate ​ 关键字,添加 实参列表 (可选),在大括号内编写需要的代码。下面是一个简单的用例:

// 省略了实参列表
EventHandler handler = delegate
{Console.WriteLine("Event raised");
};
EventHandler handler = delegate(object sender, EventArgs args)
{Console.WriteLine("Event raised. sender={0}; args={1}",sender.GetType(), args.GetType());
};

匿名方法真正的威力在闭包中才能得到体现,这部分在介绍 lambda 表达式时会进行讲解,见3.5.2 捕获变量。

2.3.3 委托的兼容性

C#1 创建委托实例时,方法 签名返回值 需要和委托的 签名返回值 完全一致。假设有如下委托声明和方法:

Printer printer = new Printer(PrintAnything);void PrintAnything(object obj)
{Console.WriteLine(obj);
}public delegate void Printer(string message);

Printer​ 委托的参数和 PrintAnything​ 的参数不一致,上述代码在 C#1 中是不合法的。C#2 开始支持了这种转换。

此外,只要委托的签名兼容,委托可以通过 委托 创建:

public delegate void GeneralPrinter(object obj);GeneralPrinter generalPrinter = ...;            // 创建任意委托
Printer printer = new Printer(generalPrinter);  // 构建一个 Printer 来封装 GeneralPrinter

同样的,只要返回值类型兼容,委托可以通过 委托 创建:

public delegate object ObjectProvider();    // 无参委托
public delegate string StringProvider();    // 有返回值StringProvider stringProvider = ...;        // 创建任意 StringProvider 委托
ObjectProvider objectProvider = new ObjectProvider(stringProvider);

Eureka

其实这部分内容涉及逆变和协变

需要注意的是,参数/返回值之间的兼容性必须满足“一致性转换”规则。以如下代码为例,它无法通过编译:

public delegate void Int32Printer(int x);
public delegate void Int64Printer(long x);Int64Printer int64Printer = ...;
Int32Printer int32Printer = new Int32Printer(int64Printer);

2.4 迭代器

2.4.1 迭代器简介

迭代器:包含迭代器块的方法或属性。

迭代器块:(本质上是)包含 yield return ​ 或 yield break ​ 语句的代码,只能用于以下返回类型的方法或属性:

  • IEnumerable
  • IEnumerable<T> ​(T​ 可以是类型形参,也可以是普通类型)
  • IEnumerator
  • IEnumerator<T> ​(T​ 可以是类型形参,也可以是普通类型)

迭代器用到的语句有:

  • yield return ​ 语句:用于生成返回序列的各个值
  • yield break ​ 语句:用于终止返回序列

下面是一个简单的迭代器例子:

static IEnumerable<int> CreateSimpleIterator()
{yield return 10;for (int i = 0; i < 3; i++){yield return i;}yield return 20;
}

2.4.2 延迟执行

迭代器的特点之一是它是延迟执行的。我们以前一节的迭代器为例,下面是一段针对它的消费代码:

IEnumerable<int> enumerable = CreateSimpleIterator();
using (IEnumerator<int> enumerator = enumerable.GetEnumerator())
{while (enumerator.MoveNext()){int value = enumerator.Current;Console.WriteLine(value);}
}
static IEnumerable<int> CreateSimpleIterator()
{yield return 10;for (int i = 0; i < 3; i++){yield return i;}yield return 20;
}

断点调试时我们会发现:CreateSimpleIterator()​、GetEnumerator()​ 方法被调用后根本不会触发断点,只有 MoveNext() ​ 被调用时才会真正开始执行。

Info

延迟执行(也称延迟计算)属于 lambda 演算的一部分,于 20 世纪 30 年代被提出。其基本思想十分简单:只在需要获取计算结果时执行代码。

2.4.3 执行 yield​ 语句

当如下几种情形之一发生时,代码会终止执行:

  • 抛出异常;

    异常会正常流转。需要注意的是,抛出异常的是 MoveNext() ​ 方法!

  • 方法执行完毕;

    MoveNext()​ 方法返回 false

  • 遇到 yield break​ 语句;

    MoveNext()​ 方法返回 false

  • 执行到 yield return​ 语句,迭代器准备返回值。

    Current ​ 属性被赋以当前迭代值,MoveNext()​ 方法返回 true

2.4.4 延迟执行的重要性

以打印斐波那契数列为例,迭代器的延迟执行的优势可以得到很大发挥:

static IEnumerable<int> Fibonacci()
{int current = 0;int next = 1;while (true)    // 只有无限次请求时才会变成无限循环{yield return current;       // 生成当前的斐波那契值int oldCurrent = current;current = next;next = next + oldCurrent;}
}static void Main()
{foreach (var value in Fibonacci()){Console.WriteLine(value);if (value > 1000){break;}}
}

从上面的例子可以看到:迭代器可以按需执行,使用者可以根据需要灵活调用 Fibonacci()​ 方法。

2.4.5 处理 finally 块

我们以如下迭代器为例,思考:“在 finally 块中”这句输出会在第一次 yield 后打印吗?

static IEnumerable<string> Iterator()
{try{Console.WriteLine("第一次 yield 前");yield return "first";Console.WriteLine("两次 yields 间");yield return "second";Console.WriteLine("第二次 yield 后");}finally{Console.WriteLine("在 finally 块中");}
}

答案是:不会。迭代器对 finally 的处理逻辑如下:

  • 每次执行至 yield return​ 语句时,执行就会 暂停 ,(逻辑上讲)执行此时还停留在 try 块中。

foreach​ 遍历迭代器时会起到 using 语句的作用(参见4.6.1 可枚举类型)。如下两段消费代码等价,都会输出“在 finally 块中”:

foreach (string value in Iterator())
{Console.WriteLine("Received value: {0}", value);if (value != null){break;}
}
IEnumerable<string> enumerable = Iterator();
using (IEnumerator<string> enumerator = enumerable.GetEnumerator())
{while (enumerator.MoveNext()){string value = enumerator.Current;Console.WriteLine("Received value: {0}", value);if (value != null){break;}}
}

在调用 Dispose()​ 方法时,即时迭代器还暂停在 try 块中也无妨,Dispose()​ 方法会最终调用 finally 块,非常智能。

2.4.6 处理 finally 的重要性

finally 块的处理对迭代器意义重大,它意味着:

  • 迭代器可以用于那些需要 释放资源 的方法;

    如文件处理器

  • 相同目的的迭代器可以 链接 起来使用

    如 LINQ to Objects

下面是一个使用迭代器读取文件的例子:

static IEnumerable<string> ReadLines(string path)
{using (TextReader reader = File.OpenText(path)){string line;while ((line = reader.ReadLine()) != null){yield return line;}}
}

通过迭代器我们只需打开文件 次。不过它也有缺点:因文件不存在/不可读导致抛出异常会延迟至 MoveNext()​ 方法调用时,导致不及时。

Tips

.NET4.0 引入了 File.ReadLines()​ 方法,功能与之类似。

Notice

需要注意的是,非泛型的 IEnumerator​ 接口并未扩展自 IDisable​ 接口(IEnumerator<T>​ 接口扩展自 IDisable​、IEnumerator​ 接口),foreach 循环会自行检查迭代器是否实现了 IDisposable​,并根据需要调用 Dispose()​ 方法。

开发者如果手动调用 MoveNext()​ 方法进行迭代,对于非泛型版 IEnumerator​,需要手动判断是否实现 IDisposable​ 接口,并调用 Dispose()​ 方法;对于泛型版 IEnumerator<T>​,直接使用 using 语句即可。

2.4.7 迭代器实现机制概览

Info

更多细节请参考Iterator 块实现细节:自动生成的状态机

迭代器明面上是调用了 yield return​、yield break​ 的方法,实际上编译器会为它生成一个全新的类型以实现相关接口。我们编写的方法体会被移动至生成类型的 MoveNext() ​ 方法中。

以如下迭代器代码为例:

public static IEnumerable<int> GenerateIntegers(int count)
{try{for (int i = 0; i < count; i++){Console.WriteLine("Yielding {0}", i);yield return i;int doubled = i * 2;Console.WriteLine("Yielding {0}", doubled);yield return doubled;}}finally{Console.WriteLine("In finally block");}
}

编译器生成的代码(反编译后、已简化)有:

public static IEnumerable<int> GenerateIntegers(    // 原方法声明签名的int count)                                      // 桩方法
{GeneratedClass ret = new GeneratedClass(-2);ret.count = count;return ret;
}
private class GeneratedClass                // 表示状态机的: IEnumerable<int>, IEnumerator<int>    // 生成类
{public int count;                       //private int state;                      // 状态机中所有不同private int current;                    // 功能的字段private int initialThreadId;            //private int i;                          //public GeneratedClass(int state)    // 桩方法和 GetEnumerator() 方法都会调用的构造器{this.state = state;initialThreadId = Environment.CurrentManagedThreadId;}public bool MoveNext() { ... }      // 状态机的主体代码public IEnumerator<int> GetEnumerator() { ... } // 用于创建新的状态机public void Reset()     // 生成的迭代器不支持 Reset 操作{throw new NotSupportedException();}public void Dispose() { ... }   // 根据需要执行 finally 块public int Current { get { return current; } }  // 用于返回最后生成值的属性private void Finally1() { ... }     // MoveNext() 和 Dispose() 方法中使用的 finally 块主体IEnumerator Enumerable().GetEnumerator()                //{                                                       //return GetEnumerator();                             // 显式实现的}                                                       // 非泛型接口成员//object IEnumerator.Current { get { return current; } }  //
}

从上述代码可以看到,编译器根据迭代器生成了一个** 状态机 **(一个私有嵌套类),状态机包含了:

  • 方法当前执行位置 指示器

    该指示器与 CPU 的指令计数器类似,用于区分若干种状态;

  • 所有 参数 的一份复本;

  • 方法体中定义的局部变量;

  • 最近一次生成的值。

调用迭代器时会执行以下操作:

  1. 调用 GetEnumerator()​ 来获得 IEnumerator<int>
  2. 反复调用 MoveNext()​ 并访问 IEnumerator<int>​ 中的 Current​ 属性,直到 MoveNext()​ 返回 false
  3. 在需要清理内存时调用 Dispose() ​ 方法,无论是否有异常抛出。

Info

生成的迭代器不支持 Reset 操作,关于 Reset 的作用,见7.1.1 IEnumerable 和 IEnumerator

MoveNext()​ 方法实现概览

MoveNext()​ 的大致结构如下:

public bool MoveNext()
{try{switch (state){// 跳转表负责跳转到方法中的正确位置}// 方法代码在每个 yield return 都会返回}fault               // 只有发生异常时 fault 块的代码才会执行,它是 IL 中专有的结构{Dispose();      // 发生异常后清理资源}
}

状态机包含了一个变量(state​)用于记录当前执行位置。变量的具体值随不同的实现有所差别。以 Roslyn 编译器为例,状态值如下所示。

  • −3:MoveNext()​ 当前正在执行。
  • −2:GetEnumerator()​ 尚未被调用。
  • −1:执行完成(无论成功与否)。
  • 0:GetEnumerator()​ 已被调用,但是 MoveNext()​ 还未被调用(方法的开始)。
  • 1:在第 1 条 yield return​ 语句。
  • 2:在第 2 条 yield return​ 语句。

MoveNext()​ 的作业逻辑有:

  1. 利用 state​ 变量在各个状态间跳转;

  2. 发生异常时:在 fault 块中执行 finally 工作

    此处会触发 Dispose() ​ 方法,进而调用 finally1()​ 方法( finally 块中的代码)。

  3. 正常执行结束:在 try 块的结尾正常调用 Dispose() ​ 方法。

Info

fault 块是一个 IL 结构,在 C# 中没有对等形式,类似于 finally 块。

2.5 一些小的特性

2.5.1 局部类型(partial class)

局部类型(也成为“分布类”)允许单个类结构体或者接口分成多个部分声明,而且一般分布于多个源文件。局部类型常与代码生成器配合使用,多个代码生成器分别负责不同的声明部分,之后还可以通过手动编码予以强化。

一个简单的示例如下:

partial class PartialDemo
{public static void MethodInPart1(){MethodInPart2();    // 调用第 2 部分声明的方法}
}partial class PartialDemo
{private static void MethodInPart2()     // 被第一部分调用的方法{Console.WriteLine("In MethodInPart2");}
}

泛型类有一些额外限制:

  • 各部分声明的类型名和类型形参 必须 相同,类型约束(如有) 必须 相同;

类型实现接口时则相对自由:

  • 声明的类型若实现了多个接口,这些局部类型 可以 负责各自的接口实现,且实现和声明 可以 不在同一局部类中

2.5.1.1 局部方法(C#3)

局部方法:可以在一个类型的局部声明中声明一个不包含 方法体 的方法,而在另一个局部声明中定义该方法的实现(可选)。

局部方法有如下特点:

  • 默认是 有方法,返回值必须是 void 且不能使用 out 参数(可以使用 ref 参数);

  • 编译时只会保留实现了的局部方法。

    如果局部方法只是声明而没有实现,那么会 移除该方法的所有调用代码

源生成 器常借助局部方法的特性生成可选的“钩子方法”,开发者可以手动为“钩子方法”添加额外的行为。

下面是局部方法的简单用例:

partial class PartialMethodsDemo
{public PartialMethodsDemo(){OnConstruction();   // 调用尚未实现的局部方法}public override string ToString(){string ret = "Original return value";CustomizeToString(ref ret);     // 调用已经实现的局部方法return ret;}partial void OnConstruction();      // 编译时会被移除partial void CustomizeToString(ref string text);
}
partial class PartialMethodsDemo
{// 局部方法的实现partial void CustomizeToString(ref string text){text += " - customized!";}
}

2.5.2 静态类

静态类:使用 static ​ 修饰符修饰的类。

静态类内部不能声明实例方法、属性、事件或构造器;可以声明普通的 嵌套 类。下面是一个简单的示例:

StaticClassDemo.StaticMethod();             // 合法StaticClassDemo localVariable = null;       // 非法
List<StaticClassDemo> list =                // 非法new List<StaticClassDemo>();static class StaticClassDemo
{public static void StaticMethod() { }   // 合法public void InstanceMethod() { }        // 非法,静态类不可声明实例方法public class RegularNestedClass         // 合法,静态类可以声明普通嵌套类型{public void InstanceMethod() { }    // 合法,普通嵌套类型可以声明实例成员}
}

此外,扩展方法(C#3)对于静态类也有特殊要求:只能在非嵌套、非泛型的静态类中声明。

2.5.3 属性的 getter/setter 访问分离

该机制于 C#2 引入:可以通过添加修饰符来让一个访问器比另一个更私有。通常都是让 setter 访问器比 getter 访问器更私有(见5.2 属性的设计)。

下面是一个简单的例子:

private string text;public string Text
{get { return text; }private set { text = value; }
}

2.5.4 命名空间别名

自 C#1 开始,C# 便支持了命名空间命名空间别名这两个特性,下面是一个简单的用例,用于区分 Windows Forms 和 ASP.NET Web Forms 的两个 Button 类:

using System;
using WinForms = System.Windows.Forms;
using WebForms = System.Web.UI.WebControls;class Test
{static void Main(){Console.WriteLine(typeof(WinForms.Button));Console.WriteLine(typeof(WebForms.Button));}
}

C#2 从 3 个方面扩展了命名空间别名的支持:

  1. 命名空间别名限定符语法
  2. 全局命名空间别名
  3. 外部别名

这部分内容参见《C#7.0 核心技术指南》:

2.12.5.2 命名空间别名限定符

在 2.12.3.3 名称隐藏中提到,内层 namespace 中的名称会隐藏外层 namespace 中的名称,我们可以通过类型的完全限定解决。不过有些情况类型的完全限定也无法解决冲突:

namespace N {class A {public class B{ }static void Main(){new A.B();        // 此处将调用内部类B}}
}namespace A {class B{ }
}

Main 方法将会实例化嵌套类 B 或命名空间 A 中的类 B。编译器总是给当前命名空间中的标识符以更高的优先级;在这种情况下,将会实例化嵌套类 B。

要解决这样的冲突,可以使用“::​”限定命名空间别名,用法如下:

1. 全局命名空间(别名 global)

即所有 namespace 的根命名空间(由上下文关键字 global 指定)

namespace N {class A {public class B{ }static void Main(){(new A.B()).GetType().Dump();(new global::A.B()).GetType().Dump();;}}
}namespace A {class B{ }
}
2.一系列的外部别名

此处代码与2.12.5.1 外部别名稍显不同:

extern alias W1;
extern alias W2;W1::Widgets.Widget w1 = new W1::Widgets.Widget();
W2::Widgets.Widget w2 = new W2::Widgets.Widget();

2.5.5 编译指令

编译指令:用于为编译器提供额外的信息,实际并不能改变程序行为。

C# 编译器支持:

  • 警告指令:主要用于禁用、启用特定警告信息

    #pragma warning disable CS0219
    int variable = CallSomeMethod();
    #pragma warning restore CS0219
    
  • 校验和指令:一般出现在自动生成的代码中

在 C#6 之前,只能使用数字来作为警告的标识。Roslyn 编译器提升了编译流的扩展性,其他包也可以提供警告信息。为此 C# 修改了警告标识规则:

  • 允许(且应该)在警告前添加前缀(如使用 CS0219 而非 0219)

Info

关于指令,另见4.16 预处理指令

2.5.6 固定大小的缓冲区

固定大小的缓冲区只能用于 非安全的 代码,并且只能用于 结构体 内部。我们通过 fixed ​ 修饰符完成这一工作。下面是一个简单的例子,它分配了 16byte 的数据:

unsafe struct VersionedData
{public int Major;public int Minor;public fixed byte Data[16];
}
unsafe static void Main()
{VersionedData versioned = new VersionedData();versioned.Major = 2;versioned.Minor = 1;versioned.Data[10] = 20;
}

Info

关于固定大小的缓冲区,另见25.6 将结构体映射到非托管内存中

C#7.3 关于访问大小固定缓冲区字段的改进

上一节的代码展示了如何通过局部变量访问固定大小的缓冲区。假设 versioned​ 变量不是局部变量,而是一个类的字段,在 C#7.3 之前,需要通过 fixed​ 语句来创建一个 指针 才能访问 versioned.Data​;到了 C#7.3 以后,就可以通过字段直接访问该缓冲区了,不过仍限于 非安全 的上下文中。

unsafe struct VersionedData
{public int Major;public int Minor;public fixed byte Data[16];
}
unsafe static void Main()
{VersionedData versioned = new VersionedData();versioned.Major = 2;versioned.Minor = 1;versioned.Data[10] = 20;
}

2.5.7 InternalsVisibleTo(友元程序集)

InternalsVisibleToAttribute​ 是作用于程序集的特性,它包含一个参数,用于指定另一个程序集。被指定的程序集可以访问当前程序集中的 internal 成员。

下面是用法的简单示例:

[assembly:InternalsVisibleTo("NodaTime.Test,PublicKey=0024...4669"]

该特性常用于:

  • 测试程序集测试当前程序集的内部成员;

  • 私有工具类访问内部成员,避免代码复制;

  • 其他库访问当前库的内部成员

    不推荐该场景使用,这致使内部成员修改时代码版本号也需随之更改

Info

关于友元程序集,另见友元程序集

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/906874.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

ESP32S3串口UART0,UART1UART2,软件模拟串口,USB虚拟串口的使用 - 基于ArduinoIDE

硬件串口的使用 硬件资源我使用的具体的模组型号为 ESP32-S3-WROOM-1(U), 根据官方手册其有3个串口。UART0:通常用于下载和输出调试信息串口,信号管脚默认与 GPIO43(TX) ~ GPIO44(RX) 复用,可以通过 GPIO 交换矩阵连接到任意 GPIO. UART1:信号管脚默认与 GPIO17(TX) ~ GPIO…

从0搭建nacos单点、集群

主机IP 主机名10.0.0.91 elk9110.0.0.92 elk9210.0.0.93 elk93nacos单机部署使用内置数据库 1.下载解压nacos [root@elk91 ~]# wget https://github.com/alibaba/nacos/releases/download/2.5.1/nacos-server-2.5.1.tar.gz[root@elk91 ~]# tar xf nacos-server-2.5.1.tar.gz -C…

ESP32在ArduinoIDE中的配置

🟡 注意 使用 Arduino IDE 开发 ESP32 时除了要看 Arduino 官方的资料一定还要看乐鑫的支持包的资料。详见↗️ 安装ArduinoIDE 到 Arduino 官网 下载最新版的 Arduino IDE 并安装。 🟡 压缩包格式的下载选项意义不大,Arduino IDE 2 无法制作为便携版,参考:绿色(Portabl…

文献阅读《Convolutional Neural Networks on Graphs with Fast Localized Spectral Filtering》

参考博客: 论文解读二代GCN《Convolutional Neural Networks on Graphs with Fast Localized Spectral Filtering》 - 别关注我了,私信我吧 - 博客园 (cnblogs.com) 摘要 为将CNN推广到高维图结构数据中,基于spectral graph theory(谱图理论),设计了一种通用的fast local…

1013 Div3 F题目加注释

https://codeforces.com/contest/2091/problem/F这题主题思路就是递推,从下往上递推,然后用差分和前缀合得到下一行可能性,详细看代码注释点击查看代码 #include <bits/stdc++.h> using namespace std; using ll = long long; using pii = pair<int, int>; const…

20244319 实验二《Python程序设计》实验报告

20244319 2024-2025-2 《Python程序设计》实验二报告 课程:《Python程序设计》 班级: 2443 姓名: 梁悦 学号:20244319 实验教师:王志强 实验日期:2025年3月26日 必修/选修: 公选课 一、实验内容 1.设计并完成一个完整的计算机应用程序,完成加、减、乘、除、log等运算,…

让 LLM 既能“看”又能“推理”!

DeepSeek-R1 会推理,GPT-4o 会看。能否让 1 LLM既能看又能推理? DeepSeek-R1取得很大成功,但它有个问题——无法处理图像输入。 1.1 DeepSeek模型发展 自2024.12,DeepSeek已发布:DeepSeek-V3(2024.12):视觉语言模型(VLM),支持图像和文本输入,类似 GPT-4o DeepSeek-…

WindowsPE文件格式入门03.节表

https://www.bpsend.net/thread-306-1-1.html dump 我们点击运行程序进程加载时时,是把文件里面的数据映射进内存,这样进程里面的内存就拿到了各种各样的代码,数据等资源,但是如果我们反着来,就可以从进程的内存里把 exe 文件提出来,这个过程叫做dump过程 dump过程在对抗里面经…

7-二次、加解密、DNS等注入

加解密注入其实就是数据被加密了,注入的时候要先把注入语句进行相应加密再注入,只是叠加了一次加密而已二次注入二次注入一般用于白盒测试,黑盒测试就算是找到注入也没办法攻击 二次注入无法通过工具或手工发现,只能观察源代码才能发现 一般产生在有数据互联的情况,比如有…

8-WAF绕过

WAF WAF部署安全狗,宝塔等waf搭建部署 https://blog.csdn.net/nzjdsds/article/details/93740686 流量防护:某ip访问过多入黑名单 建议阿里云搞个服务器部署进行绕过测试WAF常见功能总体来说,WAF(Web Application Firewall)的具有以下四个方面的功能:审计设备:用来截获所有…

一个基于 .NET 开源免费的异地组网和内网穿透工具

前言 今天大姚给大家分享一个基于 .NET 开源免费的异地组网和内网穿透工具:linker。 工具介绍 linker是一个基于 .NET8 开源免费(GPL-2.0 license)的异地组网和内网穿透工具,支持TCP(支持IPV6)打洞、UDP打洞,服务器中继,异地组网使用虚拟网卡将各个客户端组建为局域网络、…