AIGC标识 [深入解析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 层面)。
  • 基本规则

    1. 非指针类型 → dynamic 存在隐式转换
    2. dynamic 表达式 → 任意非指针类型存在隐式转换(转换也在执行期绑定)。
    3. 含有 dynamic 值的表达式,通常绑定在执行期完成。
    4. 含有 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. 有哪些有动态值参与的操作,但并非动态绑定

以下情况编译器不需要生成绑定代码

  • 赋值给 objectdynamic 变量

    无需类型转换,仅复制引用。

  • 传入参数类型为 objectdynamic 的方法

    同上,只是变量换成参数。

  • 使用 is 运算符判断类型

    d is string,不涉及绑定。

  • 使用 as 运算符转换类型

    d as string,不涉及绑定。

  • 补充说明

    • isas 不能使用用户自定义的显式/隐式转换(即使动态绑定系统支持这类转换)。
    • 几乎所有带动态值的操作,其结果类型仍为 dynamic

5. 有动态值参与但结果为静态类型的操作

即使表达式中含有 dynamic 值,编译器仍可在某些情况下确定其编译时类型

  • new SomeType(a)

    其中 adynamic,整个表达式编译时类型为 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 列的值。
  • 关键区别

    不存在 BookBooksTable 等预定义类,所有调用响应都在执行期动态完成(类似 ORM 但无静态类型)。

  • 适用场景

    动态类型在需要灵活访问数据结构的场景中颇为实用(如动态 schema 或无需预定义类的数据访问)。

2. ExpandoObject:一个装有数据和方法的动态袋子

  • 类型位置

    System.Dynamic.ExpandoObject

  • 两种工作模式(取决于是否作为动态值使用)

    1. 静态上下文:实现 IDictionary<string, object>,可作为普通字典使用(键值对集合)。
    2. 动态上下文:通过名称访问字典的键(类似属性/方法)。
  • 动态行为

    • 访问 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);
    }
}

img

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(动态属性访问,obj2dynamic
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(简化版,更易理解,性能略低)。
  • IDynamicMetaObjectProviderDynamicMetaObject

    • 接口方法: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: SomeProperty
    
    using 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. 职责划分

img

  • 三个基本层面

    • C#编译器:生成特定 IL 代码。
    • CLR:从 v2 到 v4 的优化部分受动态类型驱动,但 CLR 本身无新增变动。
    • Framework / 库:提供核心支持。
  • 动态类型涉及的库/组件

    • DLR(动态语言运行时):提供语言无关的基础架构(如 DynamicMetaObject),负责执行所有动态行为。
    • Microsoft.CSharp.dll
      • 不属于核心 Framework,可视为微软提供的“第三方”库,但与 C# 编译器强耦合。
      • 负责运行时与 C# 语言相关的特定部分(如动态参数的方法重载决议)。
      • 若代码使用动态类型,必须引用该库;否则编译报错。
      • 若未使用动态类型,可安全移除该库引用。
  • 补充说明

    编译器会生成对 Microsoft.CSharp.dll 的调用代码。动态类型是少数需要特定库支持的语言特性。

2. 动态类型生成的 IL 代码

  • 动态操作示例

    dynamic text = "hello world";
    string world = text.Substring(6);
    

    包含两个动态操作:方法调用(Substring)和结果类型转换(dynamicstring)。

  • 反编译后的关键要素

    • 编译器生成调用位置缓存类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 : dynamic
      • class 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表达式)的三项限制

  1. lambda 表达式不能直接赋值给 dynamic 变量
    • 编译器无法确定委托类型。
    • ❌ 非法:dynamic function = x => x * 2;
    • ✅ 迂回方法:先转换为具体委托类型(如 Func<dynamic, dynamic>),再赋值给 dynamic 变量。
  2. lambda 表达式不能出现在需要动态绑定的操作中
    • 例如动态调用扩展方法 source.Select(x => x * 2) 无法编译。
    • ✅ 同样可通过先转换为委托类型解决编译问题,但运行时仍会因找不到扩展方法而失败(除非调用的是实例方法)。
  3. 需要转换成表达式树的 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 动态类型的使用建议

静态类型的四大优势(作者观点)

  1. 早期错误发现
    编译时即可捕获错误,避免运行时异常(尤其对难以穷举测试的代码路径)。
  2. 编辑器智能提示
    代码补全不仅提升输入速度,更重要的是辅助开发者了解下一步可用操作(对不熟悉的类型尤其有用)。动态语言的补全准确度有限。
  3. 驱动API设计
    静态类型明确参数和返回值的类型,本身即文档;剩余不明确部分(如取值范围)只需少量注释。
  4. 执行效率
    工作发生在编译时而非运行时,效率更高(现代运行时虽强,但静态类型仍占优)。

适合使用动态类型的场景

  1. 简化反射操作
    • 需要动态访问属性/方法,且编译时知道名称但无法获取静态类型时。
    • 多步反射(如 value.SomeProperty.SomeMethod())用动态类型只需一行,用反射API需多步获取 PropertyInfoMethodInfo 等,代码冗长且可读性差。
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!");
}

  1. 处理“共有成员但不共有接口”的情况
    • 多个独立类型有同名成员,但未实现同一接口/基类(且无法修改)。
    • 动态类型可表达“成员一定存在”的意图,避免重复的类型检查和转换。
    • 作者建议:测试工程中可大胆使用;产品代码中应谨慎。
  2. 使用为动态类型构建的库
    • 如 REST 原型设计、无代码生成的 RPC API、JSON 处理等(参考 Json.NET 示例)。
    • 在数据模型尚未完全定义时,动态方式可快速推进原型开发;待模型确定后再转为静态类。

总体原则

  • 静态类型简单时优先使用静态类型
  • 根据实际场景(产品级 vs 测试/原型)权衡动态类型的得失。

4.2 可选形参和命名实参

4.2.1 带默认值的形参和带名字的实参

  • 定义

    • 可选形参:声明时使用 = 默认值 为其指定默认值。
    • 必要形参:未指定默认值的形参。
    • refout 参数不能有默认值。
  • 实参传递方式

    • 定位实参:不指定参数名,按位置匹配。
    • 命名实参:使用 参数名: 值 显式指定,可任意顺序。
  • 默认值允许的表达式

    • 编译时常量(如数值、字符串、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 如何决定方法调用的含义-方法调用的决议规则(可选形参 + 命名实参)

  • 基本规则
    1. 所有定位实参必须出现在命名实参之前(C# 7.2 后有放宽)。
    2. 定位实参按位置匹配形参(第一个定位实参 → 第一个形参,依次类推)。
    3. 命名实参按名称匹配形参,不受顺序限制
    4. 每个形参只能对应一个实参(不能重复赋值)。
    5. 所有必要形参必须提供实参。
    6. 可选形参可以省略,编译器会用默认值填充(默认值编译时嵌入 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 只作为委托的参数(输入)。
  • 代码示例

    // 协变: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>),很少需要自己声明。

4.4.3 变体的使用限制(引用转换规则与多参数检查)

注意:原文强调“99% 的情况下不会意识到使用泛型型变”,日常开发直接使用 BCL(基类库) 中已正确定义的接口(如 IEnumerable<out T>Func<in T, out TResult>)即可,无需手动推导复杂规则。

  • 核心概念

    • 类和结构体永远不变:变体声明仅存在于接口/委托,实现类(如 SimpleEnumerable<T>)本身不支持变体转换。
    • 变体转换属于引用转换:不改变运行时对象,只改变编译时类型。
    • 一致性转换:相同类型(包括 objectdynamic)的转换。
  • 关键点

    • IEnumerable<A>IEnumerable<B> 合法,当且仅当:
      • 协变 (out)AB 存在隐式引用转换一致性转换
      • 逆变 (in)BA 存在隐式引用转换一致性转换
      • 不变AB 必须为一致性转换
    • 值类型受限:装箱转换(intIConvertible)、数值转换(intlong不是引用转换,故非法。
    • 多参数逐一检查:每个类型形参独立按上述规则判定,任一参数非法则整体转换非法。
  • 合法/非法示例

    转换 是否合法 原因
    IEnumerable<string> → IEnumerable<object> ✅ 合法 子类到基类,隐式引用转换
    IEnumerable<string> → IEnumerable<IConvertible> ✅ 合法 类到实现的接口,隐式引用转换
    IEnumerable<IDisposable> → IEnumerable<object> ✅ 合法 任何引用类型到 object
    IEnumerable<object> → IEnumerable<string> ❌ 非法 objectstring 是显式引用转换,非隐式
    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 及子类 CircleRectangle;实现 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(逆变)
      • 两者都有 → 保持不变
    • 若接口作者未适当标记变体,会给使用者带来不便(例如无法将派生类型的比较器用于基类型集合)。
posted @ 2026-07-28 19:07  绘星tsuki  阅读(1)  评论(0)    收藏  举报