《Effective C#》笔记 使用C#表达设计

阅读《Effective C#》一书的总结,第三部分,使用C#表达设计。

Expressing Designs in C#

21 限制类型的可见性

在保证类型正常工作的前提下,应尽可能地给类型分配最小的可见性;

使用较低可见性的类来实现公有接口。如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// For illustration, not complete source
public class List<T> : IEnumerable<T>
{
private class Enumerator<T> : IEnumerator<T>
{
// Contains specific implementation of
// MoveNext(), Reset(), and Current.
public Enumerator(List<T> storage)
{
// elided
}
}

public IEnumerator<T> GetEnumerator()
{
return new Enumerator<T>(this);
}

// other List members.
}

22 通过定义并实现接口替代继承

基类描述了对象是什么,接口描述了对象将如何表现其行为;

扩展方法可以作用于接口,如System.Linq.Enumerable中有:

1
2
3
4
5
6
7
8
9
10
11
12
public static class Extensions
{
public static void ForAll<T>(
this IEnumerable<T> sequence,
Action<T> action)
{
foreach (T item in sequence)
action(item);
}
}
// usage
foo.ForAll((n) => Console.WriteLine(n.ToString()));

在抽象基类和接口之间做选择,实际上就表示了对日后可能发生变化的不同处理态度。借口是固定的:我们将一组功能封装在一个接口中,作为其它类型的实现契约。而基类则可以在日后进行扩展,这些扩展也会成为每个派生类的一部分;

使用接口还可以帮助避免struct类型拆箱所带来的代价。如下边的例子,可以在不拆箱的情况下直接使用IComparable.CompareTo()作比较:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public struct URLInfo : IComparable<URLInfo>, IComparable
{
private string URL;
private string description;

#region IComparable<URLInfo> Members
public int CompareTo(URLInfo other)
{
return URL.CompareTo(other.URL);
}
#endregion

#region IComparable Members
int IComparable.CompareTo(object obj)
{
if (obj is URLInfo)
{
URLInfo other = (URLInfo)obj;
return CompareTo(other);
}
else
throw new ArgumentException(
"Compared object is not URLInfo");
}
#endregion
}

使用类层次来定义相关的类型,用接口暴露功能,并可让不同的类型实现这些接口;

23 理解接口方法和虚方法的区别

接口中声明的成员并非虚方法,至少默认情况下不是虚方法。派生类不能覆写基类中实现的接口成员;

定义一个接口和两个类如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
interface IMsg
{
void Message();
}

public class MyClass : IMsg
{
public void Message()
{
Console.WriteLine("MyClass");
}
}

public class MyDerivedClass : MyClass,IMsg
{
public void Message()
//public new void Message()
{
Console.WriteLine("MyDerivedClass");
}
}

此时执行下边的代码,将会输出后边注释中的内容:

1
2
3
4
5
6
7
8
MyDerivedClass md = new MyDerivedClass ();
md.Message (); // MyDerivedClass

IMsg i = md as IMsg;
i.Message (); // MyDerivedClass

MyClass b = md as MyClass;
b.Message (); // MyClass

子类的Message()是添加了new关键字,所以改变了子类的行为,使用IMsg.Message()将会使用子类的实现,但是在转为基类类型时却使用基类的实现。为解决这一问题,通常有两种方案:

  1. 将基类中的Message()方法改为虚方法:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class MyClass : IMsg
{
public virtual void Message()
{
// ...
}
}

public class MyDerivedClass : MyClass,IMsg
{
public override void Message()
{
// ...
}
}
  1. 另一种方案是在基类实现接口时,包含对一个虚方法的调用,让子类可以覆写该虚方法:
1
2
3
4
5
6
7
8
9
10
11
public class MyClass : IMsg
{
protected virtual void OnMessage()
{
}
public void Message()
{
OnMessage();
Console.WriteLine("MyClass");
}
}

24 用委托实现回调

由于一些历史原因,.NET中的委托都是多播委托(multicast delegate)。多播委托会把所有添加到该委托中的目标函数组合成一个单一的调用。需要注意的有两点:

  1. 如果有委托调用出现异常,那么这种方式不能保证安全。委托对象本身不会捕捉任何异常,任何目标抛出的异常都会结束委托链的调用;

  2. 整个调用的返回值将作为最后一个函数调用的返回值;

为了避免上边两个问题,可以自己调用委托链上的每个委托目标,如:

1
2
3
4
5
6
7
8
9
10
11
12
public void LengthyOperation2(Func<bool> pred)
{
bool bContinue = true;
foreach (ComplicatedClass cl in container)
{
cl.DoLengthyOperation();
foreach (Func<bool> pr in pred.GetInvocationList())
bContinue &= pr();
if (!bContinue)
return;
}
}

25 用事件模式实现通知

一个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
public class LoggerEventArgs : EventArgs
{
public string Message { get; private set; }
public int Priority { get; private set; }
public LoggerEventArgs(int p, string m)
{
Priority = p;
Message = m;
}
}
public class Logger
{
static Logger()
{
theOnly = new Logger();
}
private Logger()
{
}
private static Logger theOnly = null;
public static Logger Singleton
{
get { return theOnly; }
}
// Define the event:
public event EventHandler<LoggerEventArgs> Log;
// add a message, and log it.
public void AddMsg(int priority, string msg)
{
// This idiom discussed below.
EventHandler<LoggerEventArgs> l = Log;
if (l != null)
l(this, new LoggerEventArgs(priority, msg));
}
}

注意AddMsg(...)中的临时变量l,这是触发事件的正确的方法,临时变量时一个重要的安全措施,可以预防多线程环境中的竞争条件。如果没有这个引用的副本,客户代码可能会在if判断语句和事件处理函数执行之间移除事件处理函数,而复制引用之后则可以避免这种情况;

26 避免返回对内部类对象的引用

我们不希望用户在我们不知道的情况下,通过返回的对象来访问或修改对象的内部状态。通常有四种策略来防止类型的内部数据结构遭受有意或无意地修改:

  1. 使用值类型

返回值类型时,其实返回的是值类型的副本。修改副本不会影响到对象的内部状态;

  1. 使用常量类型

返回字符串或者其它不可变类型(immutable types),客户代码不可能对其做任何更改(可结合条目20);

  1. 定义接口

使用接口将客户对内部数据成员对访问限制在一部分功能中,通过接口向外界暴露类的功能,即可尽量地避免内部数据遭受无意的更改。一个例子是使用IEnumerable<T>接口向外提供List<T>的功能;

  1. 提供包装器(wrapper)对象

仅提供包装器,从而限制对其中对象的访问。如对于集合类型的标准只读封装System.Collections.ObjectModel.ReadOnlyCollection<T>

27 让类型支持序列化

尽可能对类型添加序列化支持,大多数情况下只需要加上[Serializable]属性即可。类型可以直接添加可序列化属性的一个前提是它所有的成员都是可序列化的;

对于不想序列化的成员,使用[NonSerialized]修饰。对于使用这一属性的成员,在类型反序列化时,不会调用该成员的初始化器(它们的值只能是0或者null);

IDeserializationCallback接口提供了方法OnDeserialization(),在整个对象反序列化之后会调用该方法。但是注意在.NET Framework中反序列化时,并不能保证各成员调用顺序,因此必须确保我们的所有共有方法都能处理NonSerialized成员未被初始化时的情况;

如果考虑不同版本之间的兼容性,需要使用到ISerializable接口。实现此接口必须实现方法GetObjectData(...),用于向流中写入数据。还需要实现用于从流中构造类型的构造函数,以下是一段示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
using global::System.Runtime.Serialization;
using global::System.Security.Permissions;
[Serializable]
public sealed class MyType : ISerializable
{
private string label;
[NonSerialized]
private int value;

private OtherClass otherThing;
private const int DEFAULT_VALUE = 5;
private int value2;
// public constructors elided.
// Private constructor used only
// by the Serialization framework.
private MyType(SerializationInfo info, StreamingContext cntxt)
{
label = info.GetString("label");
otherThing = (OtherClass)info.GetValue("otherThing",
typeof(OtherClass));
try
{
value2 = info.GetInt32("value2");
}
catch (SerializationException)
{
// Found version 1.
value2 = DEFAULT_VALUE;
}
}

[SecurityPermissionAttribute(SecurityAction.Demand,
SerializationFormatter = true)]
void ISerializable.GetObjectData(SerializationInfo inf, StreamingContext cxt)
{
inf.AddValue("label", label);
inf.AddValue("otherThing", otherThing);
inf.AddValue("value2", value2);
}
}

要求SerializationFormatter安全许可用于堵住一些潜在的漏洞,确保只有受信任的代码才能访问该例程,获取对象的内部状态;

处理非密封类的序列化。如果子类要支持序列化,首先基类要支持序列化,并将序列化构造函数改为protected,还应当在GetObjectData中调用供子类覆写的用于处理数据的一个虚方法;

28 提供粗粒度的因特网服务API

与远端计算机通信时的API设计,应当同时降低通信的频率以及每次通信时传递的数据量。在二者之间找到平衡点,但更偏向于选择较少通信次数并尽量一次传输更多数据;

29 支持泛型协变和逆变

类型变体即所谓的协变(covariance)和逆变(contravariance),定义了在何种情况下,某个类型可以代替另一个类型使用;

若某个返回值的类型可以由其派生类型替换,那么这个类型就是支持协变的;若某个参数类型可以由其基类替换,那么这个类型就是支持逆变的(面向对象语言一般都支持参数类型的协变);

C#4.0开始提供了新的关键字,支持以协变和逆变的方式使用泛型。在可能的情况下,为泛型接口和委托添加inout参数;

一个协变的例子:

1
2
3
4
5
6
7
8
9
public interface IEnumerable<out T> : IEnumerable
{
IEnumerator<T> GetEnumerator();
}
public interface IEnumerator<out T> : IDisposable, IEnumerator
{
T Current { get; }
// MoveNext(), Reset() inherited from IEnumerator
}

注意IEnumerator<out T>中的out关键字,会让编译器将T的使用限制在了输出的位置,输出位置包含函数返回值、属性的get访问器以及委托参数的某些位置等;

之所以IEnumerable<T>支持协变,是因为IEnumerator<T>本身也支持协变。协变类型必须返回类型参数,或类型参数上的一个同样支持协变的接口;

可以使用in关键字,创建支持逆变(P156书中写的是协变,应该是写错了)的泛型接口和委托。这会告知编译器,该类型参数可能仅会出现在输入位置中。.NET中有这样的例子:

1
2
3
4
public interface IComparable<in T>
{
int CompareTo(T other);
}

委托参数的某些位置使用in和out关键字:方法的参数支持逆变(in),方法的返回值支持协变(out)。BCL中很多委托的定义就体现了这一点:

1
2
3
4
5
6
public delegate TResult Func<out TResult>();
public delegate TResult Func<in T, out TResult>(T arg);
public delegate TResult Func<in T1, T2, out TResult>(T1 arg1, T2 arg2);
public delegate void Action<in T>(T arg);
public delegate void Action<in T1, in T2>(T1 arg1, T2 arg2);
public delegate void Action<in T1, in T2, T3>(T1 arg1, T2 arg2, T3 arg3);

REFERENCE

《C#高效编程 改进C#代码的50个行之有效的办法》