《Effective C#》笔记(3) - 泛型
只定义刚好够用的约束条件
泛型约束可以规定一个泛型类必须采用什么样的类型参数才能够正常地运作。设定约束条件的时候,太宽或太严都不合适。 如果根本就不加约束,那么程序必须在运行的时候做很多检查,并执行更多的强制类型转换操作。而且在编译器为这个泛型类型的定义生成IL码的时候,通过约束还可以为提供更多的提示,如果你不给出任何提示,那么编译器就只好假设这些类型参数所表示的都是最为基本的System.Object,也就是假设将来的实际类型只支持由System.Object所公布的那些方法,这使得凡是没有定义在System.Object里面的用法全都会令编译器报错,甚至连最为基本的new T()等操作也不支持。
但添加约束的时候也不要过分严格,以至于限制了泛型类的使用范围,只添加确实有必要的约束即可。
创建泛型类时,应该给实现了IDisposable的类型参数提供支持
如果在泛型类里面根据类型参数创建了实例,那么就应该判断该实例所属的类型是否实现了IDisposable接口。如果实现了,就必须编写相关的代码,以防程序在离开泛型类之后发生资源泄漏。这还要分不同的情况: 泛型类的方法根据类型参数所表示的类型来创建实例并使用该实例 类似下面的写法,如果T是非托管资源,那么就会造成内存泄露:
public interface IEngine { void DoWork(); } public class EngineDriver<T> where T : IEngine, new() { public void GetThingsDone() { var driver =new T(); driver.DoWork(); } }
正确的写法应该是:
var driver =new T(); using (driver as IDisposable) { driver.DoWork(); }
编译器会把driver视为IDisposable,并创建隐藏的局部变量,用以保存指向这个IDisposable的引用。在T没有实现IDisposable的情况下,这个局部变量的值是null,此时编译器不调用Dispose(),因为它在调用之前会先做检查。反之,如果T实现了IDisposable,那么编译器会生成相应的代码,以便在程序退出using块的时候调用Dispose()方法。 这段代码等同于:
var a = driver as IDisposable; driver.DoWork(); a?.Dispose();
使用using后,需要注意的是所有调用driver实例的操作都不可以放在using区域之后,因为那时driver已经被释放了。
泛型类将根据类型参数所创建的那个实例当作成员变量 在这种情况下,那么代码会复杂一些。该类拥有的这个引用所指向的对象类型可能实现了IDisposable接口,也可能没有实现,但为了应对可能实现了IDisposable接口的情况,泛型类本身就必须实现IDisposable,并且要判断相关的资源是否实现了这个接口,如果实现了,就要调用该资源的Dispose()方法。
public class EngineDriver2<T> : IDisposable where T : IEngine, new() { // it's expensive to create, so create to null private Lazy<T> driver = new Lazy<T>(() => new T()); public void GetThingsDone() => driver.Value.DoWork(); public void Dispose() { if (driver.IsValueCreated) { var resource = driver.Value as IDisposable; resource?.Dispose(); } } }
或者可以将driver的所有权转移到该类之外,于是也就不用关心资源的释放了。|
public sealed class EngineDriver3<T> where T : IEngine { private T driver; public EngineDriver3(T driver) { this.driver = driver; } }
如果有泛型方法,就不要再创建针对基类或接口的重载版本
如果有多个相互重载的方法,那么编译器就需要判断哪一个方法应该得到调用。而在引入泛型方法之后,这套判断规则会变得更加复杂,因为只要能够替换其中的类型参数,就可以与这个泛型方法相匹配。 比如有下面三个类型,它们之间的关系如代码所示:
public class MyBase { } public interface IMsgWriter { void WriteMsg(); } public class MyDerived : MyBase, IMsgWriter { void IMsgWriter.WriteMsg() => Console.WriteLine("Inside MyDerived.WriteMsg"); }
接下来定义三个重载方法,其中包括了泛型方法:
static void WriteMsg(MyBase b) { Console.WriteLine("Inside WriteMsg(MyBase b)"); } static void WriteMsg<T>(T obj) { Console.WriteLine("Inside WriteMsg<T>(T obj)"); } static void WriteMsg(IMsgWriter obj) { Console.Write("Inside WriteMsg(IMsgWriter obj)"); }
那么如下三种调用写法,结果是怎样的呢?
MyDerived derived = new MyDerived(); WriteMsg(derived); var msgWriter = derived as IMsgWriter; WriteMsg(msgWriter); var mbase = derived as MyBase; WriteMsg(mbase);
下面为运行结果,与你预想是否一致呢?
Inside WriteMsg<T>(T obj) Inside WriteMsg(IMsgWriter obj) Inside WriteMsg(MyBase b)
第一条结果表明了一个极为重要的现象:如果对象所属的类继承自基类MyBase,那么以该对象为参数来调用WriteMsg时,WriteMsg<T>总是会先于WriteMsg(MyBase b)而得到匹配,这是因为如果要与泛型版的方法相匹配,那么编译器可以直接把子类MyDerived视为其中的类型参数T,但若要与基类版的方法相匹配,则必须将MyDerived型的对象隐式地转换成MyBase型的对象,所以,它认为泛型版的WriteMsg更好。 如果要调用到WriteMsg(MyBase b), 需要将MyDerived型的对象显式地转换成MyBase型对象,就像第三条测试那样。
如果不需要把类型参数所表示的对象设为实例字段,那么应该优先考虑创建泛型方法,而不是泛型类
一般来说,我们通常的习惯是定义泛型类,但有时更推荐用泛型方法。因为使用泛型方法时所提供的泛型参数只需与该方法的要求相符即可,而使用泛型类时所提供的泛型参数则必须满足该类所定义的每一条约束。如果将来还要给类里面添加代码,那么可能会对类级别的泛型参数施加更多的约束,从而令该类的适用场景变得越来越窄。
此外,泛型方法相比泛型类会更加灵活,比如下面的泛型工具类获取提供了获取较大值的方法:
public class Utils<T> { public static T Max(T left, T right) { return Comparer<T>.Default.Compare(left, right) > 0 ? left : right; } }
因为是泛型,那么每次调用都要提供类型:
Utils<string>.Max("c", "d"); Utils<int>.Max(4, 3);
这样虽然类本身的实现比较方便,但调用端使用起来却比较麻烦,更重要的是,值类型可以直接使用Math.Max,而不需要每次都让程序在运行的时候先去判断相关类型是否实现了IComparer<T>,然后才能调用合适的方法,Math.Max可以提供更好的性能,所以可以改进为对于值类型提供不同版本的Max方法:
public class Utils1 { public static T Max<T>(T left, T right) { return Comparer<T>.Default.Compare(left, right) > 0 ? left : right; } public static int Max(int left, int right) { return Math.Max(left, right) > 0 ? left : right; } public static double Max(double left, double right) { return Math.Max(left, right) > 0 ? left : right; } }
经过这样的修改,将泛型类改成了部分使用泛型方法,对于int、double,编译器会直接调用非泛型的版本,其它的类型会匹配到泛型版本。
Utils1.Max("c", "d"); Utils1.Max(4, 3);
这样写还有个好处是,将来如果又添加了一些针对其他类型的具体版本,那么编译器在处理那些类型的参数时就不会去调用泛型版本,而是会直接调用与之相应的具体版本。
但也要注意的是,并非每一种泛型算法都能够绕开泛型类而单纯以泛型方法的形式得以实现。 有两种情况,必须把类写成泛型类:
- 该类需要将某个值用作其内部状态,而该值的类型必须以泛型来表达(例如集合类)
- 该类需要实现泛型版的接口。
除此之外的其他情况通常都可以考虑用包含泛型方法的非泛型来实现。
只把必备的契约定义在接口中,把其他功能留给扩展方法去实现
如果程序中有很多个类都必须实现所要设计的某个接口,那么定义接口的时候就应该定义尽量少的方法,后续可以采用扩展方法的形式编写一些针对该接口的便捷方法。这样做不仅可以使实现接口的人少写一些代码,而且可以令使用接口的人能够充分利用那些扩展方法。
但使用扩展方法时需要注意一点:如果已经针对某个接口定义了扩展方法,而其他一些类又想要以它们自己的方式来实现这个同名方法,那么扩展方法就会被覆盖,类似下面这样,针对IFoo定义了扩展方法NextMarker,同时也在MyType中实现了NextMarker。
public interface IFoo { int Marker { get; set; } } public static class FooExtension { public static void NextMarker(this IFoo foo) { foo.Marker++; } } public class MyType: IFoo { public int Marker { get; set; } public void NextMarker() { this.Marker += 5; } }
那么下面代码的结果就是5,而不是1
var myType =new MyType(); myType.NextMarker(); Console.WriteLine(myType.Marker); // 5
而如果需要调用扩展方法,需要显示地将myType转换为IFoo。
var myType =new MyType(); var a = myType as IFoo; a.NextMarker();
参考书籍
《Effective C#:改善C#代码的50个有效方法(原书第3版)》 比尔·瓦格纳

低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。
持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。
转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。
- 上一篇
好未来数据中台实时数据平台演进
摘要:本文由好未来资深数据平台工程师毛祥溢分享,主要介绍批流融合在教育行业的实践。内容包括两部分,第一部分是好未来在做实时平台中的几点思考,第二部分主要分享教育行业中特有数据分析场景。大纲如下: 背景介绍 好未来 T-Streaming 实时平台 K12 教育典型分析场景 展望与规划 Tips:点击文末【链接】即可下载作者分享 PPT 并回顾原版分享视频~ 1.背景介绍 好未来介绍 好未来是一家 2003 年成立教育科技公司,旗下有品牌学而思,现在大家听说的学而思培优、学而思网校都是该品牌的衍生,2010 年公司在美国纳斯达克上市,2013 年更名为好未来。2016 年,公司的业务范围已经覆盖负一岁到 24 岁的用户。目前公司主营业务单元有智慧教育、教育领域的开放平台、K12 教育以及海外留学等业务。 好未来数据中台全景图 上图为好未来数据中台的全景图,主要分为三层: 第一层是数据赋能层 第二层是全域数据层 第三层是数据开发层 首先,数据赋能层。主要是商业智能、智慧决策的应用,包括一些数据工具、数据能力以及专题分析体系,数据工具主要包括埋点数据分析工具、AB 测试工具、大屏工具;数...
- 下一篇
一篇文章彻底搞懂GC
前言 Java相较于其他编程语言更加容易学习,这其中很大一部分原因要归功于JVM的自动内存管理机制。 对于从事C语言的开发者来说,他们拥有每一个对象的「所有权」,更大的权力也意味着更多的职责,C开发者需要维护每一个对象「从生到死」的过程,当对象废弃不用时必须手动释放其内存,否则就会发生内存泄漏。而对于Java开发者来说,JVM的自动内存管理机制解决了这个让人头疼的问题,不容易出现内存泄漏和内存溢出的问题了,GC让开发者更加专注于程序本身,而不用去关心内存何时分配、何时回收、以及如何回收。 1. JVM运行时数据区 在聊GC前,有必要先了解一下JVM的内存模型,知道JVM是如何规划内存的,以及GC的主要作用区域。 如图所示,JVM运行时会将内存划分为五大块区域,其中「方法区」和「堆」随着JVM的启动而创建,是所有线程共享的内存区域。虚拟机栈、本地方法栈、程序计数器则是随着线程的创建被创建,线程运行结束后也就被销毁了。 1.1 程序计数器 程序计数器(Program Counter Register)是一块非常小的内存空间,几乎可以忽略不计。 它可以看作是线程所执行字节码的行号指数器,指向...
相关文章
文章评论
共有0条评论来说两句吧...
文章二维码
点击排行
推荐阅读
最新文章
- CentOS7,8上快速安装Gitea,搭建Git服务器
- CentOS8安装Docker,最新的服务器搭配容器使用
- SpringBoot2配置默认Tomcat设置,开启更多高级功能
- Springboot2将连接池hikari替换为druid,体验最强大的数据库连接池
- Eclipse初始化配置,告别卡顿、闪退、编译时间过长
- Docker使用Oracle官方镜像安装(12C,18C,19C)
- CentOS8,CentOS7,CentOS6编译安装Redis5.0.7
- MySQL8.0.19开启GTID主从同步CentOS8
- SpringBoot2初体验,简单认识spring boot2并且搭建基础工程
- Windows10,CentOS7,CentOS8安装Nodejs环境