现在的位置: 首页 > 综合 > 正文

More Effective C# Item2 : 恰到好处的定义约束

2012年10月09日 ⁄ 综合 ⁄ 共 4058字 ⁄ 字号 评论关闭

    在泛型中,类型参数的约束指出了能完成该泛型类工作的类型所必须具有的行为,如果某种类型无法满足约束,那么在编译时,是不会通过的。但这也意味着在引入约束后,对于使用该泛型的用户来说,他们必须保证提供的类型满足泛型的约束,这在一定程度上,会增大使用者的工作量。在设计泛型时,是否需要添加约束,应该添加多少约束,没有一个十全十美的解决方案,我们应该尽量找到一个平衡点。

    如果我们不提供任何约束,那么就必须在运行时执行各种类型检查,例如强制类型转换,当类型不正确时,应该抛出运行时异常。但是当约束太多时,使用者就不得不为完成一些实际中可能不需要的功能,也不太好。

    对于编译器来说,当创建泛型类型时,编译器要为泛型生成合法的IL代码。当进行编译时,虽然编译器对今后可能用来替换类型参数的具体类型了解甚少,但仍需要生成合法的程序集。如果不添加任何约束,那么编译器只能假设这些类型仅具有最基本的特性,即System.Object中定义的方法。编译器无法猜测出你对类型的假设,唯一能够确认的就是该类型继承了System.Object。

    我们在使用约束后,能够得到以下两个便利:1. 有助于泛型类型的编写,编译器将会认为该泛型类型参数具有约束所定义的各种功能;2. 编译器还能保证使用该泛型类型的用户所指定的类型参数一定会满足约束的条件。

    我们来看下面的代码。

代码

1 internal class EqualTest
2 {
3 public static bool AreEqualWithOutConstraint<T>(T left, T right)
4 {
5 if (left == null)
6 {
7 return right == null;
8 }
9
10 IComparable<T> temp = left as IComparable<T>;
11 if (temp != null)
12 {
13 return temp.CompareTo(right) == 0;
14 }
15 else
16 {
17 throw new ArgumentException("Argument is not Valid !");
18 }
19 }
20
21 public static bool AreEqualWithConstraint<T>(T left, T right) where T : IComparable<T>
22 {
23 return left.CompareTo(right) == 0;
24 }
25 }

    上述代码定义了两个用于比较是否相等的方法,都是采用泛型的方式,其中一个使用了约束,一个没有使用约束。我们可以看出,使用了约束的版本,代码非常简洁。

    下面来看如何测试代码。

代码

internal class ModelWithComparable : IComparable<ModelWithComparable>
{
private string m_strValue = string.Empty;

public string Value
{
get { return m_strValue; }
set { m_strValue = value; }
}

#region IComparable<ModelWithComparable> Members

public int CompareTo(ModelWithComparable other)
{
return this.Value.CompareTo(other.Value);
}

#endregion
}

internal class ModelWithOutComparable
{
private string m_strValue = string.Empty;
public string Value
{
get { return m_strValue; }
set { m_strValue = value; }
}
}


private static void TestConstraint()
{
ModelWithComparable modelA
= new ModelWithComparable();
ModelWithComparable modelB
= new ModelWithComparable();
Console.WriteLine(EqualTest.AreEqualWithConstraint
<ModelWithComparable>(modelA, modelB));
Console.WriteLine(EqualTest.AreEqualWithOutConstraint
<ModelWithComparable>(modelA, modelB));
ModelWithOutComparable modelC
= new ModelWithOutComparable();
ModelWithOutComparable modelD
= new ModelWithOutComparable();
Console.WriteLine(EqualTest.AreEqualWithConstraint
<ModelWithOutComparable>(modelC, modelD));
Console.WriteLine(EqualTest.AreEqualWithOutConstraint
<ModelWithOutComparable>(modelC, modelD));
}

    上面的测试代码,实际上是不会编译通过的,错误出现在这一行。

Console.WriteLine(EqualTest.AreEqualWithConstraint<ModelWithOutComparable>(modelC, modelD));

    原因是ModelWithOutComparable类型没有实现IComparable<T>接口,不符合AreEqualWithConstraint<T>的约束。

    通过上面的例子,我们可以看出,如果给出恰当的约束,我们可以简化泛型方法的实现过程,至少不需要在对参数的运行时类型进行检查,不用抛出运行时异常;在有约束的情况下,关于类型的检查,都会在编译时进行。

代码

1 internal class Person : IEquatable<Person>
2 {
3 private string m_strName = string.Empty;
4 public string Name
5 {
6 get { return m_strName; }
7 set { m_strName = value; }
8 }
9 #region IEquatable<Person> Members
10
11 public bool Equals(Person other)
12 {
13 return this.Name.CompareTo(other.Name) == 0;
14 }
15
16 #endregion
17
18 public override bool Equals(object obj)
19 {
20 if (obj == null)
21 {
22 return false;
23 }
24
25 Person temp = obj as Person;
26 if (temp == null)
27 {
28 return false;
29 }
30 return this.Name.CompareTo(temp.Name) == 0;
31 }
32 }

    上述代码定义了一个Person类,实现了IEquatable<T>接口,然后我们看下面的代码。

代码

public static bool AreEqual<T>(T left, T right)
{
return left.Equals(right);
}


private static void TestEqual()
{
Person p1
= new Person();
Person p2
= new Person();
Console.WriteLine(EqualTest.AreEqual
<Person>(p1, p2));
}

    我们来看一下,上述测试代码中,当执行到Equals方法时,它会执行System.Object中的Equals方法呢,还是IEqutable<T>中的Equals方法呢,答案是前者,究其原因在于AreEqual<T>方法中没有给出任何约束,这样编译器认为使用该方法时,替换的泛型类型中只包含了System.Object中的特性,而不会有其他功能,因此不会调用到IEqutable<T>接口中的方法。

    另外一个需要注意的地方时默认构造函数约束,有时我们可以将new()约束用default()调用来代替。default是C#中的一个新的操作符,用来将变量初始化成默认值,即将值类型设置为0,将引用类型设置为null。

    我们来看一个使用default()获得类型参数默认值的例子,来看下面的代码。

代码

1 private static T FirstOrDefault<T>( IEnumerable<T> sequence, Predicate<T> test)
2 {
3 foreach (T temp in sequence)
4 {
5 if (test(temp))
6 {
7 return temp;
8 }
9 }
10
11 return default(T);
12 }

    可以看出,使用了default()的方法并不需要任何约束,而调用了new T()的方法则必须给出new约束,并且当考虑到检查空值时,值类型和引用类型的行为完全不一样。

    对于约束来说,需要特别注意以下三种类型的约束:1. new();2. struct;3. class。这三种约束表明我们对对象的创建方式给出了假定,包括该对象的默认值是0还是空引用,是否能够在该泛型类内部创建泛型类型参数实例等。我们应该尽量避免在设计泛型时使用上述三种约束。

 

    如果你想讲你的假设告知泛型类型的使用者,那么需要给出约束。不过,指定越多的约束也就意味着类的适用范围越窄。我们的最终目标是让创建出的泛型类型能够尽可能的应用到更多的场景中,因此需要在约束保证的安全性和约束给他人带来的使用上的不便之间找到平衡点。一方面努力降低所需要的假设的数量,另一方面也要把必需的假设以约束的形式确定下来。

抱歉!评论已关闭.