建议23:避免将List<T>作为自定义集合类的基类

本文探讨了在C#中实现自定义集合类时,为何应避免直接继承List&lt;T&gt;,而是实现IEnumerable&lt;T&gt;和ICollection&lt;T&gt;接口。通过示例展示了继承List&lt;T&gt;可能导致的问题,并提供了正确的实现方式。

建议23:避免将List<T>作为自定义集合类的基类

如果要实现一个自定义的集合类,不应该以一个FCL集合类为基类,反而应扩展相应的泛型接口。FCL结合类应该以组合的形式包含至自定义的集合类,需要扩展的泛型接口通常是IEnumerable<T>和ICollection<T>(或ICollection<T>的子接口,如IList<T>),前者规范了集合类的迭代功能,后者规范了一个集合通常会有的操作。

一般的情况下,下面两个实现的集合类都能完成默认的需求:

class Employees1 : List<Employee>
class Employees2 : IEnumerable<Employee>, ICollection<Employee>

不过,List<T>基本上没有提供可供子类使用的protected成员(从object中继承的Finalize和MemberwiseClone方法除外),所以继承List<T>并没有带来任何继承上的优势,反而丧失了面向接口编程的灵活性。稍加不注意,隐含的Bug就会接踵而至。

以Employees1为例,如果要在Add方法中加入某些需求方面的变化,比如,为名字添加一个后缀“Changed!",但是客户端的开发人员也许已经习惯了面向接口编程的方式,他在为集合添加一个元素是使用了如下的语法:

复制代码
        static void Main(string[] args)
        {
            Employees1 employees1 = new Employees1() 
            {
                new Employee(){ Name = "Mike" },
                new Employee(){ Name = "Rose" }
            };
            IList<Employee> employees = employees1;
            employees.Add(new Employee() { Name = "Steve" });
            foreach (var item in employees1)
            {
                Console.WriteLine(item.Name);
            }
        }

        class Employee
        {
            public string Name { get; set; }
        }

        class Employees1 : List<Employee>
        {
            public new void Add(Employee item)
            {
                item.Name += " Changed!";
                base.Add(item);
            }
        }
复制代码

于是,代码的实际输出会偏离集合类设计者的设想。代码输出为:

Mike Changed!
Rose Changed!
Steve

要纠正这类行为,应该采用Employees2的方式:

复制代码
        static void Main(string[] args)
        {
            Employees2 employees2 = new Employees2() 
            {
                new Employee(){ Name = "Mike" },
                new Employee(){ Name = "Rose" }
            };
            ICollection<Employee> employees = employees2;
            employees.Add(new Employee() { Name = "Steve" });
            foreach (var item in employees2)
            {
                Console.WriteLine(item.Name);
            }
        }

        class Employees2 : IEnumerable<Employee>, ICollection<Employee>
        {
            List<Employee> items = new List<Employee>();

            #region IEnumerable<Employee> 成员

            public IEnumerator<Employee> GetEnumerator()
            {
                return items.GetEnumerator();
            }

            #endregion

            #region ICollection<Employee> 成员

            public void Add(Employee item)
            {
                item.Name += " Changed!";
                items.Add(item);
            }

            //省略

            #endregion
        }
复制代码

输出结果为:

Mike Changed!
Rose Changed!
Steve Changed!

 

 

转自:《编写高质量代码改善C#程序的157个建议》陆敏技

斜变与逆变 1. 基本概念 协变(Covariance): • 允许使用派生类型替换基类型 • 适用于输出位置(返回值、只读场景) • 使用 out 关键字标记 逆变(Contravariance): • 允许使用基类型替换派生类型 • 适用于输入位置(方法参数) • 使用 in 关键字标记 2. 协变示例 自定义协变接口: ```c# public interface IReadOnlyList<out T> { T this[int index] { get; } int Count { get; } } ``` 协变使用: ```c# List<Employee> employees = new List<Employee>(); List<Person> personList = employees.Select(e => (Person)e).ToList(); // 协变:Employee -> Person IReadOnlyList<Person> person = new ReadOnlyList<Person>(personList); ``` 3. 逆变示例 逆变委托: ```c# // 定义接受基类参数的Action Action<Person> printPerson = (Person p) => Console.WriteLine(p.Name); // 逆变:Person -> Employee(基类作为派生类使用) Action<Employee> printEmployee = printPerson; printEmployee(new Employee { Name = "我类个豆", Company = "Google Ai" }); ``` 4. .NET 内置协变/逆变接口 协变接口: ```c# // IEnumerable<out T> - 只读枚举 IEnumerable<Employee> employees = new List<Employee>(); IEnumerable<Person> persons = employees; // 协变 // IEnumerator<out T> - 只读枚举器 IEnumerator<Employee> empEnum = employees.GetEnumerator(); IEnumerator<Person> personEnum = empEnum; // 协变 ``` 逆变接口: ```c# // IComparer<in T> - 比较器参数位置逆变 IComparer<Person> personComparer = new PersonComparer(); IComparer<Employee> employeeComparer = personComparer; // 逆变 ``` 委托协变/逆变: ```c# // Func<out TResult> - 返回值协变 Func<Employee> getEmployee = () => new Employee(); Func<Person> getPerson = getEmployee; // 协变 // Action<in T> - 参数逆变 Action<Person> actPerson = p => Console.WriteLine(p.Name); Action<Employee> actEmployee = actPerson; // 逆变 ``` 5. 协变与逆变规则 协变限制: ```c# public interface IValidCovariant<out T> { T GetValue(); // ✅ 正确:返回值位置 T ReadOnlyProperty { get; } // ✅ 正确:只读属性 } public interface IInvalidCovariant<out T> { void SetValue(T value); // ❌ 错误:参数位置不能协变 T ReadWriteProperty { get; set; } // ❌ 错误:写入位置不能协变 } ``` 逆变限制: ```c# public interface IValidContravariant<in T> { void Process(T item); // ✅ 正确:参数位置 } public interface IInvalidContravariant<in T> { T GetValue(); // ❌ 错误:返回值位置不能逆变 } ``` 6. 实际应用场景 集合处理: ```c# // 利用IEnumerable<out T>协变处理不同类型的集合 public static void ProcessPersons(IEnumerable<Person> persons) { foreach (var person in persons) { Console.WriteLine(person.Name); } } List<Employee> employees = new List<Employee>(); ProcessPersons(employees); // 协变:Employee集合作为Person集合使用 ``` 事件处理: ```c# // 事件处理器利用逆变接受更通用的参数类型 public event Action<Person> PersonEvent; Action<Employee> employeeHandler = emp => Console.WriteLine(emp.Name); PersonEvent += employeeHandler; // 逆变:Employee处理器用于Person事件 ``` 7. 注意事项 类型安全:编译器确保协变/逆变的类型安全 只读场景:协变仅适用于只读操作 参数场景:逆变仅适用于参数输入 性能影响:协变/逆变不会影响运行时性能 8. 常见误区 List 不支持协变:List<Employee> 不能直接赋值给 List<Person> 混淆协变与逆变:记住"协变是输出,逆变是输入" 过度使用:只在需要类型灵活性时使用,避免不必要的复杂性
最新发布
08-06
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值