编写高质量代码改善C#程序的157个建议[优先考虑泛型、避免在泛型中声明静态成员、为泛型参数设定约束]

前言

  泛型并不是C#语言一开始就带有的特性,而是在FCL2.0之后实现的新功能。基于泛型,我们得以将类型参数化,以便更大范围地进行代码复用。同时,它减少了泛型类及泛型方法中的转型,确保了类型安全。委托本身是一种引用类型,它保存的也是托管堆中对象的引用,只不过这个引用比较特殊,它是对方法的引用。事件本身也是委托,它是委托组,C#中提供了关键字event来对事件进行特别区分。一旦我们开始编写稍微复杂的C#代码,就肯定离不开泛型、委托和事件。本章将针对这三个方面进行说明。

  这里也有一篇之前我对泛型的简单理解篇 http://www.cnblogs.com/aehyok/p/3384637.html C#
泛型的简单理解(安全、集合、方法、约束、继承)

本文已更新至http://www.cnblogs.com/aehyok/p/3624579.html 。本文主要学习记录以下内容:

  建议32、总是优先考虑泛型

  建议33、避免在泛型类型中声明静态成员

  建议34、为泛型参数设定约束

建议32、总是优先考虑泛型

  泛型的优点是多方面的,无论是泛型类还是泛型方法都同时具备可重用性、类型安全和高效率等特性,这都是非泛型类和非泛型方法无法具备的。本建议将从可重用性、类型安全和高效率三个方面来进行剖析在实际的编码过程中为何总是应该优先考虑泛型。

一、可重用性,比如简单的设计一个集合类


    public class MyList
{
int[] items;
public int this[int i]
{
get { return items[i]; }
set { this.items[i] = value; }
}

public int Count
{
get { return items.Length; }
}

////省略一些其他方法
}

该类型只支持整型,如果要让类型支持字符串,有一种方法是重新设计一个类。但是这两个类型的属性和方法都是非常接近的,如果有一种方法可以让类型接收一个通用的数据类型,这样就可以进行代码复用了,同时类型也只要一个就够了。泛型完成的就是这样的功能。


    public class MyList<T>
{
T[] items;

public T this[int i]
{
get { return items[i]; }
set { this.items[i] = value; }
}

public int Count
{
get { return items.Length; }
}

///省略其他方法
}

  可以把T理解为一个占位符,在C#泛型编译生成的IL代码中,T就是一个占位符的角色。在运行时,即使编译器(JIT)会用实际代码中输入的T类型来代替T,也就是说,在由JIT生成的本地代码中,已经使用了实际的数据类型。我们可以把MyList<int>和MyList<string>视作两个完全不同的类型,但是,这仅是对本地代码而言的,对于实际的C#代码,它仅仅拥有一个类型,那就是泛型类型MyList<T>。

  以上从代码重用性的角度论证了泛型的优点。继续从类型MyList<T>的角度论述,如果不用泛型实现代码重用,另一种方法是让MyList的编码从object的角度去设计。在C#的世界中,所有类型(包括值类型和引用类型)都是继承自object,如果要让MyList足够通用,就需要让MyList针对object编码,代码如下:


    public class MyList
{
object[] items;
public object this[int i]
{
get { return items[i]; }
set { this.items[i] = value; }
}

public int Count
{
get { return items.Length; }
}

////省略一些其他方法
}

这会让以下代码编译通过

            MyList list = new MyList();
list[0] = 123;
list[1] = "123";

由上面两行代码带来的问题就是非”类型安全性“。该问题实际在建议20 http://www.cnblogs.com/aehyok/p/3641896.html 中已经详细论述过了。让类型支持类型安全,可以让程序在编译期间就过滤掉部分Bug,同时也能让代码规避掉”转型为object类型“或“从object转型为实际类型”所带来的效率损耗。尤其是涉及的操作类型是值类型时,还会带来装箱和拆箱的性能损耗。

例如,上文代码中的

list[1] = 123;

就会带来一次装箱操作,因为它首先倍转型为object,继而存储到items这个object数组中去了。

  泛型为C#带来的是革命性的变化,FCL之后的很多功能都是借助泛型才得到了很好的实现,如LINQ。LINQ借助于泛型和扩展方法,有效地丰富了集合的查询功能,同时避免了代码爆炸并提升了操作的性能。我们在设计自己的类型时,应充分考虑到泛型的优点,让自己的类型成为泛型类。

建议33、避免在泛型类型中声明静态成员

在上一个建议中,已经解释了应该将MyList<int> 和MyList<string>
视作两个完全不同的类型,所以,不应将MyList<T>中的静态成员理解成为MyList<int>和MyList<string>共有的成员。

对于一个非泛型类型,以下的代码很好理解:


    public class MyList
{
public static int Count { get; set; }

public MyList()
{
Count++;
}
}
class Program
{
static void Main(string[] args)
{
MyList myList1 = new MyList();
MyList mylist2 = new MyList();
Console.WriteLine(MyList.Count);
Console.ReadLine();
}
}

结果返回为2.

如果将MyList换成泛型类型,看看下面的代码会输出什么呢?


    public class MyList<T>
{
public static int Count { get; set; }

public MyList()
{
Count++;
}
}
class Program
{
static void Main(string[] args)
{
MyList<int> myList1 = new MyList<int>();
MyList<int> mylist2 = new MyList<int>();
MyList<string> mylist3 = new MyList<string>();
Console.WriteLine(MyList<int>.Count);
Console.WriteLine(MyList<string>.Count);
Console.ReadLine();
}
}

代码输出为

实际上,随着你为T指定不同的数据类型,MyList<T>相应的也变成了不同的数据类型,在它们之间是不共享静态成员的。

不过,从上文我们也觉察到了,若T所指定的数据类型是一致的,那么两个泛型对象间还是可以共享静态成员的,如上文的myList1和myList2。但是,为了规避因此而引起的混淆,仍旧建议在实际的编码工作中,尽量避免声明泛型类型的静态成员。

上面举的例子是基于泛型类型的,非泛型类型中静态泛型方法看起来很接近该例子,但是应该始终这样来理解:

非泛型类型中的泛型方法并不会在运行时的本地代码中生成不同的类型。


    public class MyList
{
public static int Count { get; set; }
public static int Func<T>()
{
return Count++;
}
}
class Program
{
static void Main(string[] args)
{
Console.WriteLine(MyList.Func<int>());
Console.WriteLine(MyList.Func<int>());
Console.WriteLine(MyList.Func<string>());
Console.ReadLine();
}
}

输出结果为

建议34、为泛型参数设定约束

”约束“这个词可能会引起歧义,有些人可能认为对泛型参数设定约束是限制参数的使用,实际情况正好相反。没有约束的泛型参数作用很有限,倒是”约束“让泛型参数具有了更多的行为和属性。


    public class Salary
{
/// <summary>
/// 姓名
/// </summary>
public string Name { get; set; }

/// <summary>
/// 基本工资
/// </summary>
public int BaseSalary { get; set; }

/// <summary>
/// 奖金
/// </summary>
public int Bouns { get; set; }

}

public class SalaryComputer
{
public int Compare<T>(T t1, T t2)
{
return 0;
}
}

查看上面定义实体类可以发现,Compare<T>方法的参数t1或参数t2仅仅具有object的属性和行为,所以几乎不能在方法中对它们做任何的操作。但是,在加了约束之后,我们会发现参数t1或参数t2变成了一个有用的对象。由于为其指定了对应的类型,t1和t2现在就是一个Salary了,在方法的内部,它拥有了属性BaseSalary和Bonus,代码如下:


    public class SalaryComputer
{
public int Compare<T>(T t1, T t2) where T:Salary
{
if (t1.BaseSalary > t2.BaseSalary)
{
return 1;
}
else if (t1.BaseSalary == t2.BaseSalary)
{
return 0;
}
else
{
return -1;
}
}
}

那么可以为泛型参数指定那些约束呢?

1、指定参数是值类型(除Nullable外),可以有如下形式:

        public void Method1<T>(T t) where T : struct
{

}

2、指定参数是引用类型,可以有如下形式:


        public void Method1<T>(T t) where T : class
{

}

public void Method1<T>(T t) where T : Salary
{

}

注意object不能用来作为约束。

3、指定参数具有无参数的公共构造函数,可以有如下形式:

        public void Method2<T>(T t) where T : new()
{

}

注意CLR目前只支持无参构造方法约束。

4、指定参数必须是指定的基类、或者派生自指定的基类。

5、指定参数必须是指定的接口、或者实现指定的接口。

6、指定T提供的类型参数必须是为U提供的参数,或者派生自为U提供的参数。


    public class Sample<U>
{
public void Method1<T>(T t) where T : U
{

}
}

7、可以对同一类型的参数设置多个约束,并且约束自身可以是泛型类型。

在编程的过程中应该始终考虑为泛型参数设定约束,正像本建议开始的时候所说,约束使泛型成为一个实实在在的“对象”,让它具有了我们想要的行为和属性,而不仅仅是一个object。

英语小贴士

1、Where is the tourist
information?——旅游咨询中心在那里?

2、Can you recommend a hotel which is not
too expensive?——是否可建议一间较为廉价的旅馆?

3、Is there an airport bus to the
city?——是否有机场巴士可到市区?

4、Is there a hotel which costs under 50
dollars a night?——是否有每晚花费在50美元以下的饭店?

5、Where is the bus stop(taxi
stand)?——巴士站牌(出租车招呼站)在那里?

6、Could you recommend a hotel in the city
center?——是否可建议一家位于市中心的旅馆?

7、Where can I get the limousine for
Hilton Hotel?——我在何处可搭乘希尔顿饭店的接泊巴士?

8、I‘d like to stay at a hotel near the
station (beach).——我想要住在靠近车站(海滩)的饭店。

作者:aehyok

出处:http://www.cnblogs.com/aehyok/

感谢您的阅读,如果您对我的博客所讲述的内容有兴趣,那不妨点个推荐吧,谢谢支持:-O。

编写高质量代码改善C#程序的157个建议[优先考虑泛型、避免在泛型中声明静态成员、为泛型参数设定约束],布布扣,bubuko.com

时间: 2024-12-15 06:53:45

编写高质量代码改善C#程序的157个建议[优先考虑泛型、避免在泛型中声明静态成员、为泛型参数设定约束]的相关文章

编写高质量代码改善C#程序的157个建议——建议45:为泛型类型参数指定逆变

建议45:为泛型类型参数指定逆变 逆变是指方法的参数可以是委托或者泛型接口的参数类型的基类.FCL4.0中支持逆变的常用委托有: Func<int T,out TResult> Predicate<in T> 常用委托有: IComparer<in T> 下面例子演示了泛型类型参数指定逆变所带来的好处: class Program { static void Main() { Programmer p = new Programmer { Name = "Mi

编写高质量代码改善C#程序的157个建议——建议27:在查询中使用Lambda表达式

建议27:在查询中使用Lambda表达式 LINQ实际上是基于扩展方法和Lambda表达式的.任何LINQ查询都能通过扩展方法的方式来代替. var personWithCompanyList = from person in personList select new { PersonName = person.Name, CompanyName = person.CompanyID==0?"Micro":"Sun" }; foreach (var item in

编写高质量代码改善C#程序的157个建议——建议26:使用匿名类型存储LINQ查询结果

建议26:使用匿名类型存储LINQ查询结果 从.NET3.0开始,C#开始支持一个新特性:匿名类型.匿名类型有var.赋值运算符和一个非空初始值(或以new开头的初始化项)组成.匿名类型有如下基本特性: 即支持简单类型也指出复杂类型.简单类型必须是一个非空初始值,复杂类型则是一个以new开头的初始化项. 匿名类型的属性是只读的,没有属性设置器,它一旦被初始化就不可更改. 如果两个匿名类型的属性值相同,那么就认为这两个匿名类型相等. 匿名类型可以再循环中用作初始化器. 匿名类型支持智能感知. 匿名

编写高质量代码改善C#程序的157个建议——建议20:使用泛型集合代替非泛型集合

建议20:使用泛型集合代替非泛型集合 在建议1中我们知道,如果要让代码高效运行,应该尽量避免装箱和拆箱,以及尽量减少转型.很遗憾,在微软提供给我们的第一代集合类型中没有做到这一点,下面我们看ArrayList这个类的使用情况: ArrayList al=new ArrayList(); al.Add(0); al.Add(1); al.Add("mike"); foreach (var item in al) { Console.WriteLine(item); } 上面这段代码充分演

编写高质量代码改善C#程序的157个建议——建议12: 重写Equals时也要重写GetHashCode

建议12: 重写Equals时也要重写GetHashCode 除非考虑到自定义类型会被用作基于散列的集合的键值:否则,不建议重写Equals方法,因为这会带来一系列的问题. 如果编译上一个建议中的Person这个类型,编译器会提示这样一个信息: “重写 Object.Equals(object o)但不重写 Object.GetHashCode()” 如果重写Equals方法的时候不重写GetHashCode方法,在使用如FCL中的Dictionary类时,可能隐含一些潜在的Bug.还是针对上一

编写高质量代码改善C#程序的157个建议——建议13: 为类型输出格式化字符串

建议13: 为类型输出格式化字符串 有两种方法可以为类型提供格式化的字符串输出.一种是意识到类型会产生格式化字符串输出,于是让类型继承接口IFormattable.这对类型来 说,是一种主动实现的方式,要求开发者可以预见类型在格式化方面的要求.更多的时候,类型的使用者需为类型自定义格式化器,这就是第二种方法,也是最灵活 多变的方法,可以根据需求的变化为类型提供多个格式化器.下面就来详细介绍这两种方法. 最简单的字符串输出是为类型重写ToString方法,如果没有为类型重写该方法,默认会调用Obj

编写高质量代码改善C#程序的157个建议——建议90:不要为抽象类提供公开的构造方法

建议90:不要为抽象类提供公开的构造方法 首先,抽象类可以有构造方法.即使没有为抽象类指定构造方法,编译器也会为我们生成一个默认的protected的构造方法.下面是一个标准的最简单的抽象类: abstract class MyAbstractClass { protected MyAbstractClass(){} } 其次,抽象类的方法不应该是public或internal的.抽象类设计的本意是让子类继承,而不是用于生成实例对象的.如果抽象类是public或internal的,它对于其它类型

编写高质量代码改善C#程序的157个建议——建议85:Task中的异常处理

建议85:Task中的异常处理 在任何时候,异常处理都是非常重要的一个环节.多线程与并行编程中尤其是这样.如果不处理这些后台任务中的异常,应用程序将会莫名其妙的退出.处理那些不是主线程(如果是窗体程序,那就是UI主线程)产生的异常,最终的办法都是将其包装到主线程上. 在任务并行库中,如果对任务运行Wait.WaitAny.WaitAll等方法,或者求Result属性,都能捕获到AggregateException异常.可以将AggregateException异常看做是任务并行库编程中最上层的异

编写高质量代码改善C#程序的157个建议——建议89:在并行方法体中谨慎使用锁

建议89:在并行方法体中谨慎使用锁 除了建议88所提到的场合,要谨慎使用并行的情况还包括:某些本身就需要同步运行的场合,或者需要较长时间锁定共享资源的场合. 在对整型数据进行同步操作时,可以使用静态类Interlocked的Add方法,这就极大地避免了由于进行原子操作长时间锁定某个共享资源所带来的同步性能损耗.回顾建议83中的例子. static void Main(string[] args) { int[] nums = new int[] { 1, 2, 3, 4 }; int total