泛型FAQ:最佳实践
内容
- 什么时候我不应该使用泛型?
- 对泛型我应该使用什么命名规范?
- 我应该在泛型接口上面添加约束吗?
- 如何处置(Dispose)泛型接口?
- 可以对一般类型参数进行类型转换吗?
- 对泛型类如何同步多线程访问?
- 如何序列化泛型类?
作者:Juval Lowy(后面介绍中说微软将此人视为Software Legend as one of the world's top .NET experts and industry leaders,名头很响亮啊。无奈本人孤陋寡闻,没听过)
注:括号里的英文是我认为不好翻译的原文,如果您有更好的译法,请告诉我,谢谢。同时,对包含generic的组合词语(如generic type parameter,generic method)参照MSDN中文网站C#泛型简介一文统一译作“一般”,虽然我认为并不好听。
什么时候我不应该使用泛型?
不使用泛型的主要原因就是跨目标(cross-targeting)——如果你要在.NET 1.1和.NET 2.0下编译相同的代码,那么由于只有.NET 2.0支持泛型,你就不能够使用泛型。
对泛型我应该使用什么命名规范?
我建议使用一个单独的大写字母来表示一般类型参数。如果你对类型参数没有其他的跟上下文有关的信息(additional contextual information),你应该使用字母T:
[C#]
public class MyClass<T>
{...}
[Visual Basic]
Public Class SomeClass(Of T)
...
End Class
[C++]
generic <typename T>
public ref class MyClass
{...};
在所有其他场合下,微软正式的对泛型的的命名规范指导是:一般类型参数要有描述性的名字,除非一个单独的字母已经表示得很清楚,再增加描述性的名字也没有多大用处。
[C#]
public interface ISessionChannel<TSession>
{...}
public delegate TOutput Converter<TInput,TOutput>(TInput from);
[Visual Basic]
Public Interface ISessionChannel(Of TSession)
...
End Interface
Public Delegate Function Converter(Of TInput, TOutput)(ByVal input As TInput) As Toutput
[C++]
generic <typename TSession>
public interface class ISessionChannel
{...};
generic <typename TInput, typename TOutput>
public delegate TOutput Converter(TInput from);
可以考虑在一般类型参数的名字中表示出添加给该一般类型参数的约束。例如,一个被约束到ISession接口的参数可以起名为TSession。
我应该在泛型接口上面添加约束吗?
接口可以为其使用的范型类型添加约束,例如:
[C#]
public interface ILinkedList<T> where T : IComparable<T>
{...}
[Visual Basic]
Public Interface ILinkedList(Of T As IComparable(Of T))
...
End Interface
[C++]
generic <typename T> where T : IComparable<T>
public interface class ILinkedList
{...};
但是,你应该小心,在接口层面上定义约束还隐含有另外一层意思。为了强调接口与实现分离的思想,接口不应该包括任何一点实现的细节。虽然有很多方法可以用来实现范型接口,但是使用特定的类型参数毕竟是一种实现的细节。约束通常情况下会更加耦合(couple)接口和特定的实现。
更好的方法是,为实现范型接口的类添加约束,保持接口本身没有约束:
[C#]
public class LinkedList<T> : ILinkedList<T> where T : IComparable<T>
{
//Rest of the implementation
}
[Visual Basic]
Public Class LinkedList(Of T As IComparable(Of T))
Implements ILinkedList(Of T)
' Rest of the implementation
End Class
[C++]
generic <typename T> where T : IComparable<T>
public ref class LinkedList : ILinkedList<T>
{
//Rest of the implementation
};
如何处置(Dispose)泛型接口?
在C#和Visual Basic中,如果你把一个一般类型参数的对象放在using语句中,编译器无法知道客户端(client)指定的实际类型是否支持IDisposable接口。因此编译器不允许在using语句中使用一般类型参数的实例。
[C#]
public class MyClass<T>
{
public void SomeMethod(T t)
{
using(t)//Does not compile
{...}
}
}
[Visual Basic]
Public Class SomeClass(Of T)
Public Sub SomeMethod(ByVal value As T)
Using value ' Does not compile
End Using
End Sub
End Class
当然,你可以强制约束类型参数支持IDisposable接口:
[C#]
public class MyClass<T> where T : IDisposable
{
public void SomeMethod(T t)
{
using(t)
{...}
}
}
[Visual Basic]
Public Class SomeClass(Of T As IDisposable)
Public Sub SomeMethod(ByVal value As T)
Using value
End Using
End Sub
End Class
但是你不应该这么做。这样做的问题在于你不能使用接口作为类型参数了,即使这个接口的基础类型(underlying type)支持IDisposable也不行:
[C#]
public interface IMyInterface
{}
public class MyOtherClass : IMyInterface,IDisposable
{...}
public class MyClass<T> where T : IDisposable
{
public void SomeMethod(T t)
{
using(t)
{...}
}
}
MyOtherClass myOtherClass = new MyOtherClass();
MyClass<IMyInterface> obj = new MyClass<IMyInterface>();//Does not compile
obj.SomeMethod(myOtherClass);
[Visual Basic]
Public Interface IMyInterface
End Interface
Public Class MyOtherClass
Implements IMyInterface, IDisposable
...
End Class
Public Class SomeClass(Of T As IDisposable)
Public Sub SomeMethod(ByVal value As T)
Using value
End Using
End Sub
End Class
Dim myOtherClass As New MyOtherClass
Dim obj As New SomeClass(Of IMyInterface) ' Does not compile
obj.SomeMethod(myOtherClass)
作为替代,我建议你在using语句里对一般类型参数使用C#中的 as 操作符或者Visual Basic中的 TryCast 操作符来允许接口作为一般类型参数使用:
[C#]
public class MyClass<T>
{
public void SomeMethod(T t)
{
using(t as IDisposable)
{...}
}
}
[Visual Basic]
Public Class SomeClass(Of T)
Public Sub SomeMethod(ByVal value As T)
Using TryCast(value, IDisposable)
End Using
End Sub
End Class
可以对一般类型参数进行类型转换吗?
对于隐式转换,编译器只允许将一般类型参数转换为object类型,或者其约束里指定的那个类型:
[C#]
interface ISomeInterface
{...}
class BaseClass
{...}
class MyClass<T> where T : BaseClass,ISomeInterface
{
void SomeMethod(T t)
{
ISomeInterface obj1 = t;
BaseClass obj2 = t;
object obj3 = t;
}
}
[Visual Basic]
Interface ISomeInterface
...
End Interface
Class BaseClass
...
End Class
Class SomeClass(Of T As{BaseClass,ISomeInterface})
Private Sub SomeMethod(ByVal value As T)
Dim obj1 As ISomeInterface = value
Dim obj2 As BaseClass = value
Dim obj3 As Object = value
End Sub
End Class
[C++]
interface class ISomeInterface
{...};
ref class BaseClass
{...};
generic <typename T> where T : BaseClass,ISomeInterface
ref class MyClass
{
void SomeMethod(T t)
{
ISomeInterface ^obj1 = t;
BaseClass ^obj2 = t;
Object ^obj3 = t;
}
};
这种隐式转换当然是类型安全的,因为无效的转换在编译时就会被发现。
对于显示转换,编译器允许将一般类型参数转换到任何接口,但是不能转换为类:
[C#]
interface ISomeInterface
{...}
class SomeClass
{...}
class MyClass<T>
{
void SomeMethod(T t)
{
ISomeInterface obj1 = (ISomeInterface)t;//Compiles
SomeClass obj2 = (SomeClass)t; //Does not compile
}
}
[Visual Basic]
Interface ISomeInterface
...
End Interface
Class BaseClass
...
End Class
Class SomeClass(Of T)
Private Sub SomeMethod(ByVal value As T)
Dim obj1 As ISomeInterface = CType(value,ISomeInterface)' Compiles
Dim obj2 As BaseClass = CType(value,BaseClass)' Does not compile
End Sub
End Class
[C++]
interface class ISomeInterface
{...};
ref class SomeClass
{...};
generic <typename T>
ref class MyClass
{
void SomeMethod(T t)
{
ISomeInterface ^obj1 = (ISomeInterface ^)t;//Compiles
SomeClass ^obj2 = (SomeClass ^)t; //Does not compile
}
};
但是,你可以通过使用一个临时的object类型变量来强制将一般类型参数转到到任何其他类型:
[C#]
class MyOtherClass
{...}
class MyClass<T>
{
void SomeMethod(T t)
{
object temp = t;
MyOtherClass obj = (MyOtherClass)temp;
}
}
[Visual Basic]
Class MyOtherClass
...
End Class
Class SomeClass(Of T)
Sub SomeMethod(ByVal value As T)
Dim temp As Object = value
Dim obj As MyOtherClass = CType(temp, MyOtherClass)
End Sub
End Class
[C++]
ref class SomeClass
{...};
generic <typename T>
ref class MyClass
{
void SomeMethod(T t)
{
Object ^temp = t;
SomeClass ^obj = (SomeClass ^)temp;
}
};
毫无疑问,这样的显示转换是很危险的,因为如果实际使用的替代一般类型参数的类型不是从你要转换到的类型那里继承的话,就可能在运行时抛出异常。
C#:为了避免这种转换时有异常的风险,一个更好的办法是使用is或者as操作符。如果一般类型参数是(is)要查询的类型,is 操作符会返回true,而as操作符会在两个类型兼容的时候执行转换,否则将返回null。
[C#]
public class MyClass{
public void SomeMethod(T t)
{
if(t is int)
{…}
if(t is LinkedList)
{…}
string str = t as string;
if(str != null)
{…}
LinkedList list = t as LinkedList;
if(list != null)
{…}
}
}
Visual Basic:为了避免这种转换时有异常的风险,一个更好的办法是使用TypeOf和TryCast操作符。如果一般类型参数是(is)要查询的类型,is 操作符会返回true。你也可以使用TryCast操作符在两个类型兼容的时候执行转换,否则就返回Nothing。
[Visual Basic]
Class SomeClass(Of T)
Public Sub SomeMethod(ByVal value As T)
If TypeOf value Is Integer Then
…
End If
If TypeOf value Is LinkedList(Of Integer, String) Then
…
End If
Dim str As String = TryCast(value,String)
If (Not str Is Nothing) Then
…
End If
Dim list As LinkedList(Of Integer, String) = _
TryCast(value,LinkedList(Of
Integer, String))
If (Not list Is Nothing) Then
…
End If
End Sub
End Class
对泛型类如何同步多线程访问?
通常来说,你不应该在一般类型参数上应用Monitor。这是因为Monitor只能用于引用类型。当你使用范型的时候,编译器不能预先判断你将会提供一个引用类型还是值类型的类型参数。在C#中,编译器会允许你使用lock语句,但是如果你提供了一个值类型作为类型参数,lock语句在运行时将不起作用。在Visual Basic中,编译器如果不能确定一般类型参数是一个引用类型,它将不允许在一般类型参数上面使用SyncLock。
在C#和Visual Basic中,唯一你可以安全地将一般类型参数锁住的时候,是你将一般类型参数限制为引用类型,要么添加约束使其为引用类型,要么从一个基类中继承:
[C#]
public class MyClass<T> where T : class
{..}
[Visual Basic]
Public Class SomeClass(Of T As Class)
...
End Class
或者:
[C#]
public class SomeClass
{...}
public class MyClass<T> where T : SomeClass
{...}
[Visual Basic]
Public Class SomeClass
...
End Class
Public Class SomeClass(Of T As SomeClass)
...
End Class
然而,通常对于同步来说,最好避免部分地锁住单独的成员变量,因为这会增加死锁的可能性。
如何序列化泛型类?
包括了一般类型参数作为成员的范型类是可以被标记为序列化的:
[C#]
[Serializable]
public class MySerializableClass<T>
{
T m_T;
}
[Visual Basic]
<Serializable()> _
Public Class MySerializableClass(Of T)
Dim m_T As T
End Class
[C++]
generic <typename T>
[Serializable]
public ref class MyClass
{
T m_T;
};
但是,在这种情况下,只有指定的类型参数可以被序列化时,范型类才可以被序列化。看下面的代码:
[C#]
public class SomeClass
{}
MySerializableClass<SomeClass> obj;
[Visual Basic]
Public Class SomeClass
End Class
Dim obj as MySerializableClass(Of SomeClass)
[C++]
public ref class SomeClass
{};
MyClass<SomeClass ^> ^obj;
obj不能被序列化,因为类型参数SomeClass不可以被序列化。因此,MySerializableClass<T>可能可以,也可能不可以被序列化,取决于使用的一般类型参数。这样可能导致在运行时丢失数据或者系统崩溃,因为客户应用程序可能不能够保持对象的状态。
目前,.NET没有提供将一般类型参数约束为可序列化的机制。解决办法是在运行时在使用这个类型之前单独进行检查,并且在任何损害发生之前马上中止使用。你可以把这个运行时的验证放在静态构造器里面:
[C#]
[Serializable]
class MySerializableClass<T>
{
T m_T;
static MySerializableClass()
{
ConstrainType(typeof(T));
}
static void ConstrainType(Type type)
{
bool serializable = type.IsSerializable;
if(serializable == false)
{
string message = "The type " + type + " is not serializable";
throw new InvalidOperationException(message);
}
}
}
[Visual Basic]
<Serializable()> _
Class SomeClass(Of T)
Private m_T As T
Shared Sub New()
ConstrainType(GetType(T))
End Sub
Private Shared Sub ConstrainType(ByVal t As Type)
If Not t.IsSerializable Then
Dim message As String = "The type " + t.ToString() + " is not
serializable"
Throw New InvalidOperationException(message)
End If
End Sub
End Class
[C++]
generic <typename T>
[Serializable]
ref class MyClass
{
T m_T;
public:
static MyClass()
{
ConstrainType(typeid<T>);
}
private:
static void ConstrainType(Type type)
{
bool serializable = type->IsSerializable;
if(serializable == false)
{
String ^message = String::Concat("The type ", type->Name,
" is not serializable");
throw gcnew SerializationException(message);
}
}
};
静态构造器对每一个应用程序域的每一个类型只执行一次,而且是在类型第一次被请求实例化之前。尽管你有一些通过编程的方式来在运行时进行判断和执行检查,但是这种在静态构造器里面执行约束验证的技术,对任何无法在编译时进行检查的约束都适用。