[深入解析C#] 第 4 章:C#4互操作性提升
C#——第 4 章:C#4互操作性提升
C#4的核心特性
-
动态类型(
dynamic)C# 兼备静态类型与动态类型(少见)。
初衷是为互操作性引入,但未成为日常常用工具。
在真正需要的场景很有用,且至少“很有趣”。
-
互操作性增强(特别是 COM 组件)
- 命名索引器
- 隐式
ref实参 - 嵌入式互操作类型
- 可选形参和命名实参(同样适用于纯托管代码,是作者日常使用的两个特性)
-
泛型型变(Generic Variance)
从 CLR v2 起就已支持,C# 4 才开放使用。
直觉上直观(如
string序列可视为object序列),但List<string>不能视为List<object>的细节较复杂。大部分时候无感知使用,但遇到问题时需理解其规则。
4.1 动态类型
游戏开发中动态类型用的非常少,可能一些编辑器或者辅助工具中会涉及,另外简化反射也较常用
4.1.1 动态类型介绍
class Program
{
static void Main()
{
dynamic text = "hello world";
string world = text.Substring(6); // 正常运行
Console.WriteLine(world);
string broken = text.SUBSTR(6); // 编译通过,IDE不报错,但运行时抛出RuntimeBinderException
Console.WriteLine(broken);
}
}
-
关键行为
- 编译器不会检查动态类型对象调用的方法是否存在(如
SUBSTR不存在也能编译) - 方法查找(绑定)发生在执行期,而非编译期
- 编译器不会检查动态类型对象调用的方法是否存在(如
-
绑定机制的转移
- 静态类型:编译器在 IL 中生成精确的方法签名调用
- 动态类型:编译器生成 IL 代码,其功能是执行绑定并执行绑定结果
- 由
dynamic关键字触发此行为
-
本质
动态类型将绑定过程从编译时转移到运行时。
1. 什么是动态类型
-
本质
dynamic只存在于 C# 中,System.Type和 CLR 中均无对应定义。- 编译器会将
dynamic变量编译为object类型,并附加[Dynamic]特性(IL 层面)。
-
基本规则
- 非指针类型 →
dynamic存在隐式转换。 dynamic表达式 → 任意非指针类型存在隐式转换(转换也在执行期绑定)。- 含有
dynamic值的表达式,通常绑定在执行期完成。 - 含有
dynamic值的表达式,其编译时类型通常也是dynamic。
- 非指针类型 →
-
规则应用示例(代码清单4-1)
dynamic text = "hello world"; // string → dynamic(规则1) string world = text.Substring(6); // 执行期绑定(规则3), // text.Substring(6) 编译时类型为 dynamic(规则4), // dynamic → string 隐式转换(规则2) -
注意事项
- 若将
world声明为int或自定义类型,编译通过,但运行时可能抛出RuntimeBinderException或尝试执行用户定义的隐式转换。
- 若将
2. 在多个上下文中应用动态绑定
-
动态类型的加法操作(代码清单4-2)
dynamic d参与d + d时,加法行为根据运行时实际类型决定:string→ 字符串拼接("texttext")int→ 数值相加(20)TimeSpan→ 时间相加(01:30:00)
class Program
{
static void Main()
{
Add("text");
Add(10);
Add(TimeSpan.FromMinutes(45));
}
static void Add(dynamic d)
{
Console.WriteLine(d + d);
}
}
texttext
20
01:30:00
-
动态方法重载决议(代码清单4-3)
调用
SampleMethod(dynamic d)时,重载选择依据动态值的运行时类型:int→ 匹配int重载decimal(包括10.5m)→ 匹配decimal重载long→ 转换为decimal(而非int),匹配decimal重载string→ 转换为object,匹配object重载- 重点:执行期重载决议会尽量与编译时行为一致,且考虑类型转换。
class Program
{
static void Main()
{
CallMethod(10);
CallMethod(10.5m);
CallMethod(10L);
CallMethod("text");
}
static void SampleMethod(int value)
{
Console.WriteLine("Method with int parameter");
}
static void SampleMethod(decimal value)
{
Console.WriteLine("Method with decimal parameter");
}
static void SampleMethod(object value)
{
Console.WriteLine("Method with object parameter");
}
static void CallMethod(dynamic d)
{
SampleMethod(d);
}
}
Method with int parameter
Method with decimal parameter
Method with decimal parameter
Method with object parameter
- 动态处理的规则
- 只有动态值才会被动态处理;静态类型值使用编译时类型。
- 经动态绑定的方法调用,其结果的编译时类型为
dynamic。 - 若选中的方法返回
void但该结果被使用(如赋值),绑定失败。 - 大多数动态操作遵循上述机制,但也有少数例外情况。
3. 在动态绑定的上下文中,编译器做哪些检查
-
可进行编译时检查的场景(即使涉及
dynamic)编译器仍会检查能否找到特定名称的方法,若找不到则引发编译错误。
适用于:
- 目标不是动态值的实例方法和索引器
- 静态方法
- 构造器
-
示例(代码清单4-4,均编译报错)
"text".Substring(0, 1, 2, d)→Substring没有4个参数的重载string.Equals<int>("foo", d)→ 泛型方法Equals<T>不存在new string(d, "broken")→ 没有接收(dynamic, string)的构造函数"text"[d, d]→ 字符串索引器不接受两个参数
class Program
{
static void Main()
{
dynamic d = new object();
int invalid1 = "text".Substring(0, 1, 2, d); // 不存在接收4个参数的Substring方法
bool invalid2 = string.Equals(d, "broken"); // 不存在泛型的Equals方法
string invalid3 = new string(d, "broken"); // 不存在接收两个参数且第二个参数为string的string构造函数
char invalid4 = "text"[d, d]; // 不存在接收两个参数的string索引器
}
}
这里看到
”text”[d]的写法,联想到C#中”text”是否是字面值而非字符串呢(像C++一样)?如果不是那”text”又是什么类型
-
结论
编译器能(但不总是)预判某些运行时错误。
使用动态绑定需格外谨慎,避免陷入不确定状态。
即便上述示例能通过编译,仍会采用动态绑定,但也有例外情况。
4. 有哪些有动态值参与的操作,但并非动态绑定
以下情况编译器不需要生成绑定代码:
-
赋值给
object或dynamic变量无需类型转换,仅复制引用。
-
传入参数类型为
object或dynamic的方法同上,只是变量换成参数。
-
使用
is运算符判断类型如
d is string,不涉及绑定。 -
使用
as运算符转换类型如
d as string,不涉及绑定。 -
补充说明
is和as不能使用用户自定义的显式/隐式转换(即使动态绑定系统支持这类转换)。- 几乎所有带动态值的操作,其结果类型仍为
dynamic。
5. 有动态值参与但结果为静态类型的操作
即使表达式中含有 dynamic 值,编译器仍可在某些情况下确定其编译时类型:
-
new SomeType(a)其中
a是dynamic,整个表达式编译时类型为SomeType(构造器调用在执行期动态绑定)。 -
a is SomeType编译时类型为
bool。 -
a as SomeType编译时类型为
SomeType。
这些表达式的类型在编译时已知,不依赖动态绑定结果。
4.1.2 超越反射的动态行为
- 动态类型的作用
- 除了让编译器和框架按常规反射执行成员调用外,还有更灵活的功能。
- 引入动态类型的重要原因
- 提升与动态语言的互操作性,允许动态语言在绑定时动态变化。
- 动态语言的典型能力
- 在执行期拦截方法调用。
- 可用于透明缓存、日志记录,或为方法动态添加未预先定义的功能或字段。
应用——1. 一个假想的数据库访问的例子
-
示例代码
dynamic database = new Database(connectionString); var books = database.Books.SearchByAuthor("HollyWebb"); foreach (var book in books) Console.WriteLine(book.Title); -
动态操作解析
Books属性 → 响应后查询数据库 schema 中的Books表,返回table对象。SearchByAuthor方法 → 识别以SearchBy开头,查找Author列,生成 SQL 并查询,返回行对象列表。Title属性 → 返回行对象中Title列的值。
-
关键区别
不存在
Book或BooksTable等预定义类,所有调用响应都在执行期动态完成(类似 ORM 但无静态类型)。 -
适用场景
动态类型在需要灵活访问数据结构的场景中颇为实用(如动态 schema 或无需预定义类的数据访问)。
2. ExpandoObject:一个装有数据和方法的动态袋子
-
类型位置
System.Dynamic.ExpandoObject -
两种工作模式(取决于是否作为动态值使用)
- 静态上下文:实现
IDictionary<string, object>,可作为普通字典使用(键值对集合)。 - 动态上下文:通过名称访问字典的键(类似属性/方法)。
- 静态上下文:实现
-
动态行为
- 访问
expando.SomeData→ 查找字典中键"SomeData"的值。 - 调用
expando.FakeMethod("hello")→ 若键对应的值是委托,且参数匹配,则执行委托并返回结果。
- 访问
-
示例总结(代码清单4-5)
- 可存储任意类型(数值、委托等)。
- 可通过
IDictionary<string, object>接口静态操作,也可动态点号访问。 - 本质是一个可动态访问的字典。
using System.Dynamic;
class Program
{
static void Main()
{
dynamic expando = new ExpandoObject();
expando.SomeData = "Some data"; // 将数据赋值给属性
// 将委托赋值给属性
Action<string> action = input => Console.WriteLine("The input was '{0}'", input);
expando.FakeMethod = action;
// 动态访问数据和委托
Console.WriteLine(expando.SomeData);
expando.FakeMethod("hello");
// 将 ExpandoObject 视作字典并打印键值
IDictionary<string, object> dictionary = expando;
Console.WriteLine("Keys: {0}",
string.Join(", ", dictionary.Keys));
// 使用静态上下文填充数据,然后从动态值获取数据
dictionary["OtherData"] = "other";
Console.WriteLine(expando.OtherData);
}
}

IDE 中提示类型为???(动态)
-
应用场景
可用来实现前述“假想数据库”示例(如
Books表用ExpandoObject,每本书也用ExpandoObject),但实际更常用DynamicObject或直接实现IDynamicMetaObjectProvider。
3. Json.Net的动态视图
- Json.NET 处理 JSON 的方式
- 解析为自定义类
- 解析为类似 LINQ to XML 的对象模型(LINQ to JSON,类型如
JObject,JArray,JProperty)
- 两种访问方式对比(代码清单4-6)
- 静态类型视图:
obj1["address"]["town"](使用字符串索引器) - 动态类型视图:
obj2.address.town(动态属性访问,obj2为dynamic)
- 静态类型视图:
using Newtonsoft.Json.Linq;
class Program
{
static void Main()
{
// 硬编码的 JSON 数据
string json = @"
{
'name': 'John Doe',
'address': {
'town': 'Reading',
'country': 'UK'
}
}".Replace('\'', '"');
// 将 JSON 解析成 JObject
JObject obj1 = JObject.Parse(json);
// 使用静态视图
Console.WriteLine(obj1["address"]?["town"]);
// 使用动态视图
dynamic obj2 = obj1;
Console.WriteLine(obj2.address.town);
}
}
- 优缺点
- 静态方式:字符串作为属性名,可复用度高。
- 动态方式:原型设计时更便于阅读,但同样容易犯拼写错误。
- 两种方式都存在拼写风险。
4. 用代码实现动态行为
-
实现动态行为的两种底层方式
- 直接实现
IDynamicMetaObjectProvider接口(极其复杂,需处理DynamicMetaObject和表达式树)。 - 继承
DynamicObject类(简化版,更易理解,性能略低)。
- 直接实现
-
IDynamicMetaObjectProvider与DynamicMetaObject- 接口方法:
GetMetaObject,返回DynamicMetaObject。 DynamicMetaObject负责动态绑定的具体逻辑,如BindInvokeMember等,需创建表达式树表示调用行为。- 本书不要求手动实现这一层。
- 接口方法:
-
DynamicObject(推荐实践方式)通过重写虚方法实现动态行为,例如:
TryInvokeMember—— 处理方法调用。TryGetMember—— 处理属性获取。- 方法返回
true表示成功,false会抛出RuntimeBinderException。 - 示例中实现了:任何方法调用打印方法名+参数;任何属性获取返回
"Fetched: 属性名"。
-
应用示例
dynamic example = new SimpleDynamicExample(); example.CallSomeMethod("x", 10); Console.WriteLine(example.SomeProperty); // 输出: // Invoked: CallSomeMethod(x,10) // Fetched: SomePropertyusing System.Dynamic; using Newtonsoft.Json.Linq; class Program { static void Main() { dynamic example = new SimpleDynamicExample(); example.CallSomeMethod("x", 10); Console.WriteLine(example.SomeProperty); } } class SimpleDynamicExample : DynamicObject { // 处理方法调用 public override bool TryInvokeMember(InvokeMemberBinder binder, object?[]? args, out object? result) { Console.WriteLine("Invoked: {0} ({1})", binder.Name, string.Join(", ", args)); result = null; return true; } // 处理属性访问 public override bool TryGetMember(GetMemberBinder binder, out object? result) { result = "Fetched: " + binder.Name; return true; } }using System.Dynamic; using System.Collections; class Program { static void Main() { dynamic database = new Database("Server=localhost;Database=Library;Trusted_Connection=True"); var books = database.Books.SearchByAuthor("Holly Webb"); foreach (var book in books) { Console.WriteLine($"书名: {book.Title}"); Console.WriteLine($"作者: {book.Author}"); Console.WriteLine($"出版年份: {book.Year}"); Console.WriteLine("---"); } } } public class Database : DynamicObject { private readonly string _connectionString; public Database(string connectionString) { _connectionString = connectionString; } public override bool TryGetMember(GetMemberBinder binder, out object? result) { result = new DynamicTable(_connectionString, binder.Name); return true; } } public class DynamicTable : DynamicObject { private readonly string _connectionString; private readonly string _tableName; public DynamicTable(string connectionString, string tableName) { _connectionString = connectionString; _tableName = tableName; } public override bool TryInvokeMember(InvokeMemberBinder binder, object?[]? args, out object? result) { var methodName = binder.Name; var parameters = args ?? Array.Empty<object>(); result = ExecuteQuery(methodName, parameters); return true; } private IEnumerable<DynamicRow> ExecuteQuery(string methodName, object?[] parameters) { var mockData = GetMockData(); if (methodName.StartsWith("SearchBy")) { var propertyName = methodName["SearchBy".Length..]; return mockData.Where(row => string.Equals( row.GetProperty(propertyName)?.ToString(), parameters.FirstOrDefault()?.ToString(), StringComparison.OrdinalIgnoreCase ) ); } return mockData; } private List<DynamicRow> GetMockData() { return new List<DynamicRow> { new DynamicRow(new Dictionary<string, object> { { "Title", "The Tale of Pip Squeak" }, { "Author", "Holly Webb" }, { "Year", 2015 } }), new DynamicRow(new Dictionary<string, object> { { "Title", "The Secret Kitten" }, { "Author", "Holly Webb" }, { "Year", 2016 } }), new DynamicRow(new Dictionary<string, object> { { "Title", "Harry Potter" }, { "Author", "J.K. Rowling" }, { "Year", 1997 } }) }; } } public class DynamicRow : DynamicObject, IEnumerable<KeyValuePair<string, object>> { private readonly Dictionary<string, object> _data; public DynamicRow(Dictionary<string, object> data) { _data = data; } public override bool TryGetMember(GetMemberBinder binder, out object? result) { if (_data.TryGetValue(binder.Name, out var value)) { result = value; return true; } result = null; return false; } public object? GetProperty(string name) { _data.TryGetValue(name, out var value); return value; } public IEnumerator<KeyValuePair<string, object>> GetEnumerator() { return _data.GetEnumerator(); } IEnumerator IEnumerable.GetEnumerator() { return GetEnumerator(); } }
4.1.3 动态行为机制速览
1. 职责划分

-
三个基本层面
- C#编译器:生成特定 IL 代码。
- CLR:从 v2 到 v4 的优化部分受动态类型驱动,但 CLR 本身无新增变动。
- Framework / 库:提供核心支持。
-
动态类型涉及的库/组件
- DLR(动态语言运行时):提供语言无关的基础架构(如
DynamicMetaObject),负责执行所有动态行为。 - Microsoft.CSharp.dll:
- 不属于核心 Framework,可视为微软提供的“第三方”库,但与 C# 编译器强耦合。
- 负责运行时与 C# 语言相关的特定部分(如动态参数的方法重载决议)。
- 若代码使用动态类型,必须引用该库;否则编译报错。
- 若未使用动态类型,可安全移除该库引用。
- DLR(动态语言运行时):提供语言无关的基础架构(如
-
补充说明
编译器会生成对
Microsoft.CSharp.dll的调用代码。动态类型是少数需要特定库支持的语言特性。
2. 动态类型生成的 IL 代码
-
动态操作示例
dynamic text = "hello world"; string world = text.Substring(6);包含两个动态操作:方法调用(
Substring)和结果类型转换(dynamic→string)。 -
反编译后的关键要素
- 编译器生成调用位置缓存类(
Callsites),为每个动态操作(方法调用、类型转换)缓存CallSite。 CallSite位于System.Runtime.CompilerServices,与语言无关。Binder类(如Binder.InvokeMember,Binder.Convert)位于Microsoft.CSharp.RuntimeBinder,负责 C# 特定的绑定逻辑。
- 编译器生成调用位置缓存类(
-
效率机制
- 动态类型通过多级缓存提升性能,每次绑定结果被缓存,后续调用直接使用。
- 虽然不及静态类型,但动态类型性能已接近高效,在大部分实用场景中不会成为瓶颈。
-
注意事项
- 反编译后的代码较为复杂,非兴趣驱动无需深究。
- 使用动态类型必须引用
Microsoft.CSharp.dll,否则编译报错。
4.1.4 动态类型的局限与意外
1. 动态类型与泛型
- 非法声明(编译错误)
- 类型实现的接口中不能包含
dynamic类型实参(如IEnumerable<dynamic>) - 泛型类型约束中不能使用
dynamic(如where T : dynamic) - 具体非法示例:
class DynamicSequence : IEnumerable<dynamic>class DynamicListSequence : IEnumerable<List<dynamic>>class DynamicConstraint1<T> : IEnumerable<T> where T : dynamicclass DynamicConstraint2<T> : IEnumerable<T> where T : List<dynamic>
- 类型实现的接口中不能包含
- 合法声明
- 类的基类可以包含
dynamic类型实参(如List<dynamic>) dynamic可以用作变量的接口类型实参(如IEnumerable<dynamic> x = ...)- 嵌套在基类或变量声明中的
dynamic是允许的合法示例:class DynamicList : List<dynamic>class ListOfDynamicSequences : List<IEnumerable<dynamic>>IEnumerable<dynamic> x = new List<dynamic> { 1, 0.5 }.Select(x => x * 2);
- 类的基类可以包含
2. 扩展方法
-
执行期绑定器不能处理扩展方法
原因:需要在每个调用点保存所有
using指令的附加信息,实现代价过高。 -
静态绑定调用依然有效
即使类型实参包含
dynamic(如List<dynamic>),调用扩展方法(如Select)仍属于编译时绑定,可以正常使用。class Program { static void Main() { List<dynamic> source = new List<dynamic> { 5, 2.75, TimeSpan.FromSeconds(45) }; IEnumerable<dynamic> query = source.Select(x => x * 2); foreach (dynamic value in query) { Console.WriteLine(value); } } } -
动态目标上调用扩展方法会失败
若将
source声明为dynamic,然后调用source.Any(),会抛出RuntimeBinderException,因为运行时绑定器找不到Any这个扩展方法。 -
变通方案:以普通静态方法形式调用
将扩展方法作为静态方法显式调用(如
Enumerable.Any(source)),此时绑定发生在执行期,但仅涉及重载决议(不依赖扩展方法机制)。
3. 动态类型与匿名函数(lambda表达式)的三项限制
- lambda 表达式不能直接赋值给
dynamic变量- 编译器无法确定委托类型。
- ❌ 非法:
dynamic function = x => x * 2; - ✅ 迂回方法:先转换为具体委托类型(如
Func<dynamic, dynamic>),再赋值给dynamic变量。
- lambda 表达式不能出现在需要动态绑定的操作中
- 例如动态调用扩展方法
source.Select(x => x * 2)无法编译。 - ✅ 同样可通过先转换为委托类型解决编译问题,但运行时仍会因找不到扩展方法而失败(除非调用的是实例方法)。
- 例如动态调用扩展方法
- 需要转换成表达式树的 lambda 表达式不能包含任何动态操作
- 示例:
IQueryable<dynamic>上调用Select(x => x * 2)会编译报错,因为x * 2是动态操作,无法生成表达式树。 - 该限制在实际使用中影响有限,因为动态类型与表达式树的组合意义不明确。
- 示例:
4. 匿名类型
-
匿名类型的本质
编译后生成普通类,访问级别为
internal(仅对所在程序集可见)。 -
动态访问匿名类型属性的可行性
在同一程序集内,可以通过
dynamic访问匿名类型的属性(如obj.Name)。绑定器仅检查访问权限,不关心是否匿名类型。
-
跨程序集访问的问题
若将动态访问匿名类型的代码拆分到不同程序集,会抛出
RuntimeBinderException,因为绑定器发现属性所在类型为internal,无法访问。 -
解决方案
使用
[InternalsVisibleTo]特性,让调用方程序集可以访问匿名类型所在程序集的内部成员。
5. 显式的接口实现
显式接口实现在游戏开发中很少用,想让类实现某个接口但不暴露某些方法,只能通过接口调用时可以考虑使用
-
问题现象
通过
dynamic调用显式实现的接口成员会失败(抛出RuntimeBinderException)。 -
原因
执行期绑定器使用动态值的具体类型(而非接口类型)进行绑定,因此无法访问显式实现的接口成员(该类成员仅通过接口视图可见)。
-
示例
List<int>显式实现了IList.IsFixedSize。- 静态
List<int>变量直接访问IsFixedSize→ 编译错误。 IList类型变量访问 → 成功(返回false)。dynamic变量访问 → 执行期错误。
-
解决方法
先将动态值转换为目标接口类型(通过显式转换或中间变量),再进行调用。
namespace GameSample;
public class Program
{
public static void Main()
{
var player = new Player("Hero");
var enemy = new Enemy("Goblin");
IMovable movable = player;
movable.Move(10, 5);
IDamageable damageable = enemy;
damageable.TakeDamage(20);
if (enemy is IAttackable attacker)
{
attacker.AttackTarget(player);
}
}
}
public interface IMovable
{
void Move(float x, float y);
}
public interface IDamageable
{
int Health { get; }
void TakeDamage(int damage);
}
public interface IAttackable
{
int AttackPower { get; }
void AttackTarget(IDamageable target);
}
public class Player : IMovable, IDamageable
{
public string Name { get; }
public int Health { get; private set; } = 100;
public Player(string name) => Name = name;
public void Move(float x, float y) =>
Console.WriteLine($"[{Name}] Move to ({x}, {y})");
public void TakeDamage(int damage)
{
Health = Math.Max(0, Health - damage);
Console.WriteLine($"[{Name}] HP: {Health}");
}
}
public class Enemy : IMovable, IDamageable, IAttackable
{
public string Name { get; }
public int Health { get; private set; } = 50;
public int AttackPower => 20;
public Enemy(string name) => Name = name;
public void Move(float x, float y) =>
Console.WriteLine($"[{Name}] Moves to ({x}, {y})");
public void TakeDamage(int damage)
{
Health = Math.Max(0, Health - damage);
Console.WriteLine($"[{Name}] HP: {Health}");
}
void IAttackable.AttackTarget(IDamageable target) // 显式接口实现
{
Console.WriteLine($"[{Name}] Attack {AttackPower} damage!");
target.TakeDamage(AttackPower);
}
}
4.1.5 动态类型的使用建议
静态类型的四大优势(作者观点)
- 早期错误发现
编译时即可捕获错误,避免运行时异常(尤其对难以穷举测试的代码路径)。 - 编辑器智能提示
代码补全不仅提升输入速度,更重要的是辅助开发者了解下一步可用操作(对不熟悉的类型尤其有用)。动态语言的补全准确度有限。 - 驱动API设计
静态类型明确参数和返回值的类型,本身即文档;剩余不明确部分(如取值范围)只需少量注释。 - 执行效率
工作发生在编译时而非运行时,效率更高(现代运行时虽强,但静态类型仍占优)。
适合使用动态类型的场景
- 简化反射操作
- 需要动态访问属性/方法,且编译时知道名称但无法获取静态类型时。
- 多步反射(如
value.SomeProperty.SomeMethod())用动态类型只需一行,用反射API需多步获取PropertyInfo、MethodInfo等,代码冗长且可读性差。
using System;
using System.Reflection;
namespace ReflectionVsDynamic;
public class Program
{
public static void Main()
{
var config = new GameConfig();
var info = new CharacterInfo();
Console.WriteLine("===== 反射 API =====");
ReflectionWay(config);
Console.WriteLine("\n===== dynamic =====");
DynamicWay(config);
Console.WriteLine("\n===== 链式调用对比 =====");
Console.WriteLine("反射:");
ReflectionChainWay(info);
Console.WriteLine("dynamic:");
DynamicChainWay(info);
}
static void ReflectionWay(GameConfig config)
{
Type type = config.GetType();
PropertyInfo? settingProp = type.GetProperty("Setting");
object? setting = settingProp?.GetValue(config);
if (setting != null)
{
PropertyInfo? nameProp = setting.GetType().GetProperty("GameName");
string? gameName = nameProp?.GetValue(setting) as string;
Console.WriteLine($"Game Name: {gameName}");
}
}
static void DynamicWay(GameConfig config)
{
dynamic dynamicConfig = config;
string gameName = dynamicConfig.Setting.GameName;
Console.WriteLine($"Game Name: {gameName}");
}
static void ReflectionChainWay(CharacterInfo info)
{
PropertyInfo? weaponProp = info.GetType().GetProperty("Weapon");
object? weapon = weaponProp?.GetValue(info);
if (weapon != null)
{
PropertyInfo? damageProp = weapon.GetType().GetProperty("Damage");
int damage = (int)(damageProp?.GetValue(weapon) ?? 0);
Console.WriteLine($"Weapon Damage: {damage}");
}
}
static void DynamicChainWay(CharacterInfo info)
{
dynamic dynamicInfo = info;
int damage = dynamicInfo.Weapon.Damage;
Console.WriteLine($"Weapon Damage: {damage}");
}
}
public class GameConfig
{
public ConfigSetting Setting { get; set; } = new ConfigSetting();
}
public class ConfigSetting
{
public string GameName { get; set; } = "MyGame";
public int MaxPlayers { get; set; } = 4;
}
public class CharacterInfo
{
public WeaponInfo Weapon { get; set; } = new WeaponInfo();
}
public class WeaponInfo
{
public string Name { get; set; } = "Sword";
public int Damage { get; set; } = 50;
public void Attack() => Console.WriteLine("Attack!");
}
- 处理“共有成员但不共有接口”的情况
- 多个独立类型有同名成员,但未实现同一接口/基类(且无法修改)。
- 动态类型可表达“成员一定存在”的意图,避免重复的类型检查和转换。
- 作者建议:测试工程中可大胆使用;产品代码中应谨慎。
- 使用为动态类型构建的库
- 如 REST 原型设计、无代码生成的 RPC API、JSON 处理等(参考 Json.NET 示例)。
- 在数据模型尚未完全定义时,动态方式可快速推进原型开发;待模型确定后再转为静态类。
总体原则
- 静态类型简单时优先使用静态类型。
- 根据实际场景(产品级 vs 测试/原型)权衡动态类型的得失。
4.2 可选形参和命名实参
4.2.1 带默认值的形参和带名字的实参
-
定义
- 可选形参:声明时使用
= 默认值为其指定默认值。 - 必要形参:未指定默认值的形参。
ref或out参数不能有默认值。
- 可选形参:声明时使用
-
实参传递方式
- 定位实参:不指定参数名,按位置匹配。
- 命名实参:使用
参数名: 值显式指定,可任意顺序。
-
默认值允许的表达式
- 编译时常量(如数值、字符串、
null)。 default(T)表达式(C# 7.1 后可用default)。new 值类型()(如new Guid(),new CancellationToken())。
- 编译时常量(如数值、字符串、
-
顺序规则
- 所有可选形参必须放在必要形参之后。
- 形参数组(
params)与可选形参同时存在时,虽合法但易混淆,建议避免。
-
示例(代码清单4-15)
void Method(int x, int y = 5, int z = 10) { ... } Method(1, z: 3); // x=1, y=5, z=3 Method(z: 3, y: 2, x: 1); // 命名实参乱序,仍匹配正确 -
编译器行为
当省略可选实参时,编译器在调用处嵌入默认值。
4.2.2 如何决定方法调用的含义-方法调用的决议规则(可选形参 + 命名实参)
- 基本规则
- 所有定位实参必须出现在命名实参之前(C# 7.2 后有放宽)。
- 定位实参按位置匹配形参(第一个定位实参 → 第一个形参,依次类推)。
- 命名实参按名称匹配形参,不受顺序限制。
- 每个形参只能对应一个实参(不能重复赋值)。
- 所有必要形参必须提供实参。
- 可选形参可以省略,编译器会用默认值填充(默认值编译时嵌入 IL)。
- 重要注意事项
- 实参求值顺序:严格按照源代码中从左到右的顺序执行。命名实参的顺序会影响副作用发生的次序,可能导致不同结果。
- 建议:如果实参求值有副作用,先赋值给局部变量,再用这些变量作为实参,确保行为可预测。
- 默认值的嵌入:编译器将可选形参的默认值直接嵌入 IL,因此默认值必须是编译时常量(或
default(T)、值类型的new T())。这也意味着修改默认值会影响版本号(需重新编译调用方)。
- 实参求值顺序:严格按照源代码中从左到右的顺序执行。命名实参的顺序会影响副作用发生的次序,可能导致不同结果。
4.2.3 对版本号的影响
-
参数名称改变是破坏性变更
若更改公共方法的参数名(如
x改为a),使用命名实参的调用方会因名称不存在而失败。参数名应像类型名一样谨慎对待。 -
默认值改动的版本影响
- 默认值在调用方编译时嵌入 IL,而非运行时获取。
- 同一程序集内无影响;不同程序集需重新编译调用方才能生效新默认值。
- 规避方法:用
Nullable<int>等类型,默认值设为null,让方法运行时自行选择实际值(行为变更无需重编译)。
-
添加重载方法需格外小心
- 可选形参不属于方法签名,改为可选/非可选不破坏二进制兼容性,但可能破坏源码兼容性。
- 添加含更多可选形参的重载可能引发重载决议歧义(匹配度相同时,无可选形参的方法优于有可选但未提供实参的方法;两个都有可选形参则不分胜负)。
- 建议:避免添加带可选形参的重载;改用“选项类”作为参数(类中属性可动态扩展,不改变原方法签名)。
4.3 COM 互操作性提升
此部分等需要时再进行学习补充
4.4 泛型形变
讨论内容:如何根据泛型的类型实参对泛型进行安全的类型转换
4.4.1 泛型型变示例
-
核心概念:
- 协变:泛型参数只用作输出(如返回值),允许将派生类型隐式转换为基类型(如
IEnumerable<string>→IEnumerable<object>)。 - 逆变:泛型参数只用作输入(如方法参数),允许将基类型隐式转换为派生类型(如
Action<object>→Action<string>)。 - 不变:泛型参数既用作输入也用作输出,不允许任何隐式转换(如
IList<T>)。
- 协变:泛型参数只用作输出(如返回值),允许将派生类型隐式转换为基类型(如
-
关键点:
- C# 4 之前不支持协变/逆变,
IEnumerable<string>不能赋给IEnumerable<object>。 IEnumerable<T>是协变的(out T),因为T只从序列中读取(输出)。IList<T>是不变的,因为它有Add(T)(输入)和索引器get/set(输入+输出)。若允许转换,会导致类型安全问题(向IList<object>添加object后,原IList<string>取出时类型错误)。Action<T>是逆变的(in T),因为T只作为委托的参数(输入)。
- C# 4 之前不支持协变/逆变,
-
代码示例:
// 协变:IEnumerable<out T> IEnumerable<string> strings = new List<string> { "a", "b", "c" }; IEnumerable<object> objects = strings; // C# 4+ 合法 // 不变:IList<T> —— 以下代码非法 IList<string> stringList = new List<string> { "a", "b", "c" }; // IList<object> objectList = stringList; // 编译错误 // 逆变:Action<in T> Action<object> objectAction = obj => Console.WriteLine(obj); Action<string> stringAction = objectAction; // 合法 stringAction("Print me"); -
设计意图与安全原则:
- 协变安全:只能取出基类型,不能插入,因此派生序列可视为基序列。
- 逆变安全:只能传入派生类型的值,委托预期基类型,实际接收派生类型依然安全(里氏替换)。
- 不变:同时支持读和写时,必须保持类型完全一致才能避免运行时异常。
4.4.2 接口和委托声明中的变体语法(in/out 修饰符)
-
核心概念:
- 变体仅适用于接口和委托,类、结构体不支持变体。
- 每个类型形参可单独标记:
out T:协变(T 只用作输出,如返回值)in T:逆变(T 只用作输入,如参数)- 无修饰符:不变(T 既用作输入也用作输出)
-
关键点:
- 编译器强制检查修饰符的使用合法性:
out参数不能出现在输入位置(如方法参数)。in参数不能出现在输出位置(如返回值、get访问器)。
- 同一声明中多个类型形参可混合使用修饰符(如
Func<in T, out TResult>)。
- 编译器强制检查修饰符的使用合法性:
-
代码示例:
// 正确声明 public interface IEnumerable<out T> // 协变 public delegate void Action<in T> // 逆变 public interface IList<T> // 不变 // 非法声明示例 public delegate void InvalidCovariant<out T>(T input); // out 用作输入 → 错误 public interface IInvalidContravariant<in T> { T GetValue(); } // in 用作输出 → 错误 // 多形参示例 public delegate TResult Func<in T, out TResult>(T arg); // T 逆变,TResult 协变 -
注意事项:
- 日常开发中通常直接使用 .NET 已定义好的变体接口/委托(如
IEnumerable<out T>、Action<in T>、Func<in T, out TResult>),很少需要自己声明。
- 日常开发中通常直接使用 .NET 已定义好的变体接口/委托(如
4.4.3 变体的使用限制(引用转换规则与多参数检查)
注意:原文强调“99% 的情况下不会意识到使用泛型型变”,日常开发直接使用 BCL(基类库) 中已正确定义的接口(如
IEnumerable<out T>、Func<in T, out TResult>)即可,无需手动推导复杂规则。
-
核心概念:
- 类和结构体永远不变:变体声明仅存在于接口/委托,实现类(如
SimpleEnumerable<T>)本身不支持变体转换。 - 变体转换属于引用转换:不改变运行时对象,只改变编译时类型。
- 一致性转换:相同类型(包括
object↔dynamic)的转换。
- 类和结构体永远不变:变体声明仅存在于接口/委托,实现类(如
-
关键点:
- 从
IEnumerable<A>到IEnumerable<B>合法,当且仅当:- 协变 (
out):A→B存在隐式引用转换或一致性转换。 - 逆变 (
in):B→A存在隐式引用转换或一致性转换。 - 不变:
A与B必须为一致性转换。
- 协变 (
- 值类型受限:装箱转换(
int→IConvertible)、数值转换(int→long)不是引用转换,故非法。 - 多参数逐一检查:每个类型形参独立按上述规则判定,任一参数非法则整体转换非法。
- 从
-
合法/非法示例:
转换 是否合法 原因 IEnumerable<string> → IEnumerable<object>✅ 合法 子类到基类,隐式引用转换 IEnumerable<string> → IEnumerable<IConvertible>✅ 合法 类到实现的接口,隐式引用转换 IEnumerable<IDisposable> → IEnumerable<object>✅ 合法 任何引用类型到 objectIEnumerable<object> → IEnumerable<string>❌ 非法 object→string是显式引用转换,非隐式IEnumerable<string> → IEnumerable<Stream>❌ 非法 不相关类 IEnumerable<int> → IEnumerable<IConvertible>❌ 非法 装箱转换,非引用转换 IEnumerable<int> → IEnumerable<long>❌ 非法 数值转换,非引用转换 -
多参数委托示例(
Func<in T, out TResult>):// 合法:Func<object, int> → Func<string, int> // - T 逆变:string → object 隐式引用转换 ✅ // - TResult 协变:int → int 一致性转换 ✅ // 合法:Func<dynamic, string> → Func<object, IConvertible> // - T 逆变:dynamic → object 一致性转换 ✅ // - TResult 协变:string → IConvertible 隐式引用转换 ✅ // 非法:Func<string, int> → Func<object, int> // - T 逆变:object → string 不是隐式引用转换 ❌(整体失败,无需检查第二参数) -
设计意图:
- 确保类型安全:值类型转换会改变表示形式(装箱/拆箱/数值提升),不能作为引用转换处理。
- 多数开发者无需牢记完整规则,仅用于调试编译错误。
-
与前后文的关联:
- 前文给出了
in/out语法声明,本节细化了转换的具体判定条件。 - 下一节将介绍“泛型型变的具体应用场景”(原文末句)。
- 前文给出了
4.4.4 泛型形变实例
-
核心概念:
泛型型变在日常开发中常隐式发挥作用,但显式理解有助于简化代码(如避免不必要的
Cast调用)以及利用IComparer<T>的逆变实现派生类型的复用排序。 -
关键点:
1. LINQ 与
IEnumerable<out T>协变- 问题:对
IEnumerable<string>查询后,希望直接得到List<object>(而非List<string>)。 - C# 4 之前:需要显式调用
.Cast<object>()进行转换。 - C# 4 之后:利用
IEnumerable<out T>协变,直接在.ToList<object>()中指定目标类型。- 原理:
.Where返回IEnumerable<string>,它可视为IEnumerable<object>(协变转换),然后ToList<object>消费该视图。
- 原理:
2.
IComparer<T>逆变- 场景:定义基类
Shape及子类Circle、Rectangle;实现IComparer<Shape>按面积排序。 - 需求:对
List<Circle>使用同一个比较器进行排序。 - C# 4 之前:无法直接传递
IComparer<Shape>给List<Circle>.Sort。 - C# 4 之后:
IComparer<T>是逆变的(in T),因此IComparer<Shape>可以隐式转换为IComparer<Circle>,直接传入Sort方法。- 安全原因:比较器只读取
Circle对象(输入),将其视为Shape比较,完全安全。
- 安全原因:比较器只读取
- 问题:对
-
代码示例:
// 1. LINQ 协变简化 IEnumerable<string> strings = new[] { "a", "b", "cdefg", "hij" }; // C# 4+ 写法:无需 Cast<object> List<object> list = strings .Where(x => x.Length > 1) .ToList<object>(); // IEnumerable<string> 协变为 IEnumerable<object> // 2. IComparer<T> 逆变应用 class Shape { public double Area { get; } } class Circle : Shape { public double Radius { get; } } class Rectangle : Shape { } class AreaComparer : IComparer<Shape> { public int Compare(Shape x, Shape y) => x.Area.CompareTo(y.Area); } List<Circle> circles = new List<Circle> { /* ... */ }; circles.Sort(new AreaComparer()); // IComparer<Shape> → IComparer<Circle> 逆变转换 ✅ -
最佳实践与设计建议:
- 在 LINQ 管道中,若最终需要的集合类型是基类(如
object),可直接在ToList<基类>中指定,省去Cast。 - 自定义接口或委托时,应主动评估类型形参的角色:
- 只输出 → 标记
out(协变) - 只输入 → 标记
in(逆变) - 两者都有 → 保持不变
- 只输出 → 标记
- 若接口作者未适当标记变体,会给使用者带来不便(例如无法将派生类型的比较器用于基类型集合)。
- 在 LINQ 管道中,若最终需要的集合类型是基类(如

浙公网安备 33010602011771号