总结
关键字in的用法
- 泛型接口和委托中的泛型类型参数。
- in(里面,内部,子类) 参数类型允许它的子类; out(外面,外部,基类) 返回类型允许它的基类.
public delegate TResult Func<in T, out TResult>(T arg); // 原型Func<Object, ArgumentException> fn1 = null; // 声明Func<String, Exception> fn2 = fn1; // 使用不需要显式转型
- 作为参数修饰符,它允许按引用而不是按值向方法传递参数。
- foreach 语句。
- LINQ 查询表达式中的 from 子句。
- LINQ 查询表达式中的 join 子句。
关键字ref的用法
- 要求在传递之前初始化变量
- 在方法签名和方法调用中,按引用将参数传递给方法。。
- 在方法签名中,按引用将值返回给调用方。
- 在成员正文中,指示引用返回值是否作为调用方欲修改的引用被存储在本地,或在一般情况下,局部变量按引用访问另一个值。
- 在
struct声明中声明ref struct或readonly ref struct。- 永远不能在作为另一类的成员的堆上创建这些类型的实例
关键字out的用法
- 它与
ref关键字相似,只不过ref要求在传递之前初始化变量 - 作为
out参数传递的变量在方法调用中传递之前不必进行初始化.但是,被调用的方法需要在返回之前赋一个值.
接口
CLR通过接口提供了’缩水版’的多继承.
接口是引用类型. 实际是对一组方法签名进行统一命名. 这些方法不提供任何实现.类通过指定接口名称来继承接口. 必须显式实现接口方法.
凡是能使用具体名称接口类型的实例的地方, 都能使用实现了接口的类型的实例.
定义接口
接口对一组方法签名进行了统一命名.
接口能定义:
- 方法
- 事件
- 无参属性
- 有参属性(索引器)
以上东西本质上都是方法,只是语法上的简化.
但是接口不能定义:
- 任何构造器方法
- 任何实例字段
- 静态方法
- 静态字段
- 常量
- 静态构造器
接口定义可以从另一个或多个接口”继承”,但这不是严格意义上的继承, 是将其他接口的协定包括到新接口中.
// 接口"继承"接口
// 1. 继承接口ICollection<T>的任何类必须实现 ICollection<T> ,IEnumerable<T>,IEnumerable这3个接口所定义的方法
// 2. 任何代码在引用对象时,如果实现了ICollection<T>接口,可以认为也实现了IEnumerable<T>,IEnumerable接口
public interface ICollection<T> : IEnumerable<T>,IEnumerable
{...}
继承接口
C#编译器要求:实现接口的方法标记为public
CLR要求:将接口方法标记virtual
- 如果显式标记为
virtual,编译器就会将该方法标记为virtual(保持它的非密封状态),使派生类能重写它. - 如果不显式标记为
virtual,编译器就会标记为virtual和sealed, 这会阻止派生类重写接口方法.
派生类不能重写sealed的接口方法. 但派生类可以重新继承同一个接口,并为接口方法提供自己的实现.
internal static class InterfaceReimplementation
{
public static void Go()
{
/************************* 第一个例子 *************************/
Base b = new Base();
// 结果显示: "Base's Dispose"
b.Dispose();
// 接口类型的变量 IDisposable b; 调用接口方法
// 用b的对象的类型来调用Dispose,结果显示: "Base's Dispose"
((IDisposable)b).Dispose();
/************************* 第二个例子 ************************/
Derived d = new Derived();
// 结果显示: "Derived's Dispose"
d.Dispose();
// 用d的对象的类型来调用Dispose,结果显示: "Derived's Dispose"
((IDisposable)d).Dispose();
/************************* 第三个例子 *************************/
b = new Derived();
// 用b的类型来调用Dispose,结果显示: "Base's Dispose"
// 因为子类是自己new覆盖父类的方法,并不会修改父类方法的功能
// 所以用Base类型的对象去调用dispose会输出Base类的方法
b.Dispose();
// 用b的对象的类型来调用Dispose,显示: "Derived's Dispose"
// 转成接口类型后, 基类的方法被隐藏,只有子类的方法
// 只要子类实现了接口方法,就会调用实际类型的实现
((IDisposable)b).Dispose();
/************************* new和override **********************/
// Base b;
b.Test();
// 如果 子类实现Test 用new关键字, 则输出: Base Test!
// 如果 子类重写Test 用override关键字, 则输出: Dervied Test!
}
}
// 这个类型派生自 Object 并且实现了 IDisposable
internal class Base : IDisposable
{
// 这个方法是隐式密封的,不能被重写
// 没标记virtual实现接口方法,是不能被子类重写的
public void Dispose()
{
Console.WriteLine("Base's Dispose");
}
// 基类的方法
public virtual void Test()
{
Console.WriteLine("Base Test!");
}
}
// 这个类继承了Base并且实现了IDisposable接口
// 派生类可以重新继承同一个接口,并为接口方法提供自己的实现.
// new 是覆盖,不会改变父类方法的功能
internal class Derived : Base, IDisposable
{
// 这个方法不能重写 Base's Dispose.
// 'new' 关键字表明重新实现了IDisposable的Dispose
new public void Dispose()
{
Console.WriteLine("Derived's Dispose");
// 注意: 下一行展示了如何让调用基类的方法
// base.Dispose();
}
public new void Test()
{
Console.WriteLine("Dervied Test!");
}
}
override: 重写:会重写基类的方法,如果子类转基类,用基类对象去调用也会执行子类的实现
new: 覆盖(隐藏):不会改变父类方法的功能,并隐藏基类的方法,如果子类转基类,用基类对象去调用会执行基类的实现.(不会判断实际的类型,只会从当前对象类型去调用)
转型接口变量调用接口方法和用类的实例调用接口方法是不同的.
- 基类继承接口, 子类如果要实现接口方法需要加
new. - 用
类的实例去调用(new)接口方法时,会执行当前类型的接口方法,不会去判断实际类型. - 转成
接口变量,去调用接口方法,会根据实际类型,如果实际类型(子类)实现了接口方法,就会隐藏基类实现的接口方法.调用实际(子类)类型的实现.
关于调用接口方法的更多探讨
CLR允许定义接口类型的字段,参数或局部变量.
使用接口类型的变量可以调用该接口定义的方法.并且还可以调用Object定义的方法.
但是 不能 用接口变量调用类本身定义的公共方法.
值类型可实现0个或多个接口,但是值类型的实例转换为接口类型时必须装箱.因为接口变量是引用.
隐式和显式接口方法实现(幕后发生的事情)
类型加载到CLR中时,会为该类型创建并初始化一个方法表。在这个方法表中,类型引入的每个新方法都有对应的记录项;另外,还为该类型继承的所有虚方法添加了记录项目。继承的虚方法既有继承层次结构中的各个基类型定义的,也有接口类型定义的。
interter sealed class SimpleType : IDisposable {
public void Dispose() { console.WriteLine("Dispose"); }
}
接口方法签名和新增方法签名(相同的参数和返回类型)一致, 如果标记为virtual,C#编译器仍然会认为该方法匹配接口方法.
C#编译器将新方法和接口方法匹配起来之后,会生成元数据,指明SimpleType类型的方法表中的两个记录项应该引用同一个实现.
下面的代码演示了如果调用类的公共Dispose方法以及如何调用IDisposable的Dispose方法在类中的实现:
public static void Main()
{
SimpleType st = new SimpleType();
// 调用公共的 Dispose 方法实现
st.Dispose();
// 调用 IDisposable 的 Dispose 方法实现
IDisposable d = st;
d.Dispose();
}
// 输出:
// Dispose
// Dispose
- 在第一个dispose方法调用中,调用的是SimpleType定义的dispose方法。
- 然后定义IDisposable接口类型的变量d,它引用SimpleType对象。
- 调用SimpleType时,调用的是IDisposable接口的dispose方法。
- 由于
公共dispose方法是IDisposable的Dispose方法的实现,所以会执行相同的代码。
为了看出区别
public sealed class SimpleType : IDisposable
{
public void Dispose() { Console.WriteLine("public Dispose"); }
// 显示接口方法实现 EIMI
// 不允许显示指定可访问性,会正在编译时自动设为private,防止其他类型直接调用
// 派生类也不可以调用
// 只能由接口类型变量才能调用
void IDisposable.Dispose() { Console.WriteLine("IDisposable Dispose"); }
}
// 这样一下代码就会输出
st.Dispose();
IDisposable d = st;
d.Dispose();
// public Dispose
// IDisposable Dispose
显示接口方法实现EIMI,
- 不能显式指定可访问性, 编译时生成private
- 派生类也无法调用
- 只有接口类型变量才能调用接口方法
- 不能标记virtual,所以不能被重写(这是因为EIMI方法并不是真的是类型的对象模型的一部分),只是将接口和类型连接起来,同时避免公开行为/方法.
泛型接口
泛型接口提供了出色的编译时安全性. 非泛型接口是Object参数,值类型会产生装箱,并且缺少类型安全.
private static void SomeMethod1()
{
Int32 x = 1, y = 2;
IComparable c = x;
// CompareTo 期望接口一个 Object 类型; 传递 y (一个 Int32 类型) 允许
c.CompareTo(y); // y在这里装箱
// CompareTo期望接口一个 Object 类型; 传递 "2" (一个 String 类型) 允许
// 但运行是抛出 ArgumentException 异常
c.CompareTo("2");
}
// 修改后
private static void SomeMethod2()
{
Int32 x = 1, y = 2;
IComparable<Int32> c = x;
// CompareTo 期望接口一个 Int32 类型; 传递 y (一个 Int32 类型) 允许
c.CompareTo(y); // y在这里不装箱
// CompareTo 期望接口一个 Int32 类型; 传递 "2" (一个 String 类型) 编译不通过
// 指出 String 不能被隐式转型为 Int32
// c.CompareTo("2");
}
FCL中有些泛型接口并未实现,而且继承了非泛型接口,如果接口的任何翻翻获取或返回Object,就会失去编译时的类型安全性,而且值类型将发生装箱, 所以利用显示接口方法EIMI实现来增强编译时类型安全性.
泛型接口的好处还有,类可以实现同一个接口若干次,只要每次使用的类型参数不同.
public static void Go()
{
Number n = new Number();
// n 与 一个 Int32类型 5 作比较
IComparable<Int32> cInt32 = n;
Int32 result = cInt32.CompareTo(5);
// n 与一个 String类型 "5" 作比较
IComparable<String> cString = n;
result = cString.CompareTo("5");
}
// 该类实现了 IComparable<T> 接口两次
public sealed class Number : IComparable<Int32>, IComparable<String>
{
private Int32 m_val = 5;
// 该方法实现了 IComparable<Int32>’s CompareTo
public Int32 CompareTo(Int32 n)
{
return m_val.CompareTo(n);
}
// 该方法实现了 IComparable<String>’s CompareTo
public Int32 CompareTo(String s)
{
return m_val.CompareTo(Int32.Parse(s));
}
}
泛型接口还可以标记为逆变in和协变out,为泛型接口使用提供了更大的灵活性.
逆变in: 允许传入参数使用T的基类
协变out:允许输出类型使用T的派生类
逆变参数基类,协变返回子类delegate Object Method(FileStream fs); // 允许协变和逆变的转换String Method2(Stream s);
泛型和接口约束
泛型类型参数约束为多个接口,这样传递的参数必须要实现全部接口约束.
public sealed class SomeType
{
private static void Test()
{
Int32 x = 5;
Guid g = new Guid();
// 对M的调用能通过编译,因为Int32实现了IComparable 和 IConvertible
// 不会发生装箱
M(x);
// 对M的调用能不通过编译,因为Guid实现了IComparable,但没实现了 IConvertible
// M(g);
}
// M类型参数T被约束为需要支持同时实现IComparable 和 IConvertible interfaces接口的类型
private static Int32 M<T>(T t) where T : IComparable, IConvertible
{
// ...
return 0;
}
// 如果这样声明,x传给M就必须装箱, 接口是引用类型.
private static Int32 M<IComparable t>{..}
}
这很有用,, 如果参数的类型是接口, 那么实参可以是任意类类型,只要该类实现了此接口.
使用多个接口约束,实际上是表示向方法传递的实参必须实现多个接口.
接口约束还可以减少传递值类型实例时的装箱,M(x);上述代码向M方法传递了x(值类型int实例)。x传给M方法时不会发生装箱。M内部的代码调用t.CompareTo(..),这个调用本身也不会引发装箱,但传给CompareTo的实参可能发生装箱.
C#编译器为接口约束生成特殊的IL指令,导致直接在值类型上调用接口方法而不装箱,不用接口约束就没办法生成特殊指令. 在值类型调用接口时总是装箱,例外是这个值类型实现了一个接口方法.在值类型的实例上调用这个方法不会造成值类型的实例装箱.
实现多个具有相同方法名和签名的接口
要定义一个实现了这两个接口的类型,必须使用显示接口方法实现来实现这个类型的成员.
public interface IWindow
{
Object GetMenu();
}
public interface IRestaurant
{
Object GetMenu();
}
// 这个类型派生自 System.Object and
// 并不实现 IWindow 和 IRestaurant 接口.
public class MarioPizzeria : IWindow, IRestaurant
{
// 这是IWindow 的 GetMenu 方法.
Object IWindow.GetMenu()
{
// ...
return null;
}
// 这是 IRestaurant 的 GetMenu 方法.
Object IRestaurant.GetMenu()
{
// ...
return null;
}
// 这个GetMenu方法是可选的,与接口无关
public Object GetMenu()
{
// ...
return null;
}
}
这个类要实现多个接口的GetMenu方法,所以要告诉编译器每个GetMenu对应的是哪个接口的实现.在使用使用必须将对象转换为具体的接口才能调用所需的方法.
public static void Go()
{
MarioPizzeria mp = new MarioPizzeria();
// 这行调用 MarioPizzeria 的公共 GetMenu 方法
mp.GetMenu();
// 这行调用 MarioPizzeria 的 IWindow.GetMenu 方法
IWindow window = mp;
window.GetMenu();
// 这行调用 MarioPizzeria 的 IRestaurant.GetMenu 方法
IRestaurant restaurant = mp;
restaurant.GetMenu();
}
用显式接口方法实现EIMI来增强编译时类型安全性
因为有些接口不存在泛型版本,或者泛型版本是继承非泛型版本,所以仍需实现非泛型接口.
非泛型接口接收任何System.Object类型的参数或返回System.Object类型的值,这样就会失去编译时的类型安全性,装箱也会发生.
// 这个接口定义了一个方法,该方法接受一个System.Object类型的参数。
// 可像下面一样实现该接口的一个类型:
public interface IComparable {
Int32 CompareTo(Objetc other);
}
// 值类型实现接口
internal struct SomeValueType : IComparable
{
private Int32 m_x;
public SomeValueType(Int32 x) { m_x = x; }
public Int32 CompareTo(Object other)
{
return (m_x - ((SomeValueType)other).m_x);
}
}
public static void Go()
{
SomeValueType v = new SomeValueType(0);
Object o = new Object();
Int32 n = v.CompareTo(v); // 出现装箱,因为参数是Object,V是值类型
n = v.CompareTo(o); // 能通过编译,会InvaidCastException抛出转换异常
}
为了解决上述代码的装箱问题和类型安全性问题(编译期就能报错,而不是运行时)
internal struct SomeValueType : IComparable
{
private Int32 m_x;
public SomeValueType(Int32 x) { m_x = x; }
// 改动: 参数类型换成了SomeValueType,这样就不会发生值类型转Object时发生装箱操作
public Int32 CompareTo(SomeValueType other)
{
// 也不需要强制类型转换的操作
return (m_x - other.m_x);
}
// 因为修改了上面的CompareTo方法, 所以还需要实现接口的公共CompareTo方法来满足接口协定,这就是IComparable.CompareTo方法的作用
// 注意: 这个是显示实现接口EIMI 没有指定public或者private的可访问性
Int32 IComparable.CompareTo(Object other)
{
return CompareTo((SomeValueType)other);
}
}
经过这样修改之后, 就不存在装箱问题,编译时期就能报错,不用在运行时才报,有了类型安全性.
但如果定义接口类型的变量会再次失去编译时的类型安全性,也会发生装箱.(发生2次)
public static void Go()
{
SomeValueType v = new SomeValueType(0);
// 接口类型是引用类型, 值类型赋值会装箱
IComparable c = v; //第一次装箱
Object o = new Object();
// 这里使用的是接口类型变量去调用接口方法IComparable.CompareTo(Object other)
// 接口类型变量只能调用接口定义的方法,
Int32 n = c.CompareTo(v); // 第二次发生装箱操作
// n = c.CompareTo(o); // 运行时InvalidCastException异常
}
实现IConvertible, ICollection,IList和IDictionary等接口时,可利用EIMI为这些接口的方法创建类型安全的版本,并减少值类型的装箱。
谨慎使用显式接口方法实现
使用EIMI也可能造成一些严重后果,所以应该尽量避免使用EIMI。幸好,泛型接口可帮助我们在大多数时候避免使用EIMI。但有时,比如实现具有相同名称和签名的两个接口方法时,仍需要它们。
EIMI最主要的问题如下:
- 没有文档解释一个类型具体如何实现一个EIMI方法,也没有vs的智能感知。
- 值类型的实例在转型为接口时装箱
- EIMI不能由派生类调用
// 问题1,2
// 不能直接从一个Int32上调用一个IConvertible接口方法
// 错误说明:int不包含ToSingle的定义,但是实际上是定义了
// 必须先转换为IConvertible接口变量,才能调用, 还会装箱,并损害性能
public static void Main()
{
int x=5;
// 无法编译此句
// Single s=x.ToSingle(null);
// 修改成这样才能编译
// 但是这又会发生装箱,浪费内存又损害性能
Single s=((IConvertible) x).ToSingle(null);
}
// 问题3
// 不能由派生类调用
internal class Base : IComparable
{
// EIMI 显示接口方法实现
// 此方法只能通过IComparable接口类型变量来调用
int IComparable.CompareTo(object obj)
{
Console.WriteLine("Base's CompareTo");
return 0;
}
}
internal sealed class Derived : Base, IComparable
{
public int CompareTo(object obj)
{
Console.WriteLine("Derived's CompareTo");
// 情况一
// 试图调用基类的EIMI导致编译错误
// error CS0117: Base不包含CompareTo的定义
// base.CompareTo(obj);
// 情况二, 修改情况一
// 试图调用基类的EIMI导致无穷递归
// 通过接口变量调用接口方法, 会根据实际类型,调用到Derived的CompareTo
// 就产生无限递归
// 解决的方法是 internal sealed class Derived : Base { } 去掉IComparable接口定义
IComparable c = this;
c.CompareTo(obj);
return 0;
}
}
为了解决问题3中情况二,这样的解决方式有时不能因为想在派生类中实现接口方法就将接口从类型中删除,正确定义Base类和Derived类的代码如下:
// 用于解决 调用基类的接口方法
// 在基类中定义个用于派生类的虚方法,
// 这样 如果用接口类型变量就能访问到 基类的CompareTo方法,并且不会产生无限递归
internal static class Program
{
public static void Main()
{
Base b = new Base();
Derived d = new Derived();
IComparable c = d;
// 输出
// Derived's CompareTo
// Base's virtual CompareTo
d.CompareTo(b);
}
}
internal class Base : IComparable
{
// EIMI 显示接口方法实现
Int32 IComparable.CompareTo(object obj)
{
Console.WriteLine("Base's CompareTo");
// 调用虚方法
return CompareTo(obj);
}
// 用于派生类的虚方法(名字可以任意)
public virtual Int32 CompareTo(Object o)
{
Console.WriteLine("Base's virtual CompareTo");
return 0;
}
}
internal sealed class Derived : Base, IComparable
{
// 公共方法,也是接口的实现
public override Int32 CompareTo(object obj)
{
Console.WriteLine("Derived's CompareTo");
// 现在可以调用基类的虚接口方法
return base.CompareTo(obj);
}
}
EIMI在某些情况下确实有用,但是应该 尽量避免使用,这样导致类型变得很不好用.
设计:基类还是接口
选择基类还是接口的指导性原则:
IS-Avs.CAN-DO关系
类型 只能继承一个实现。如果派生类型和基类型不能建立起is–a关系,就不用基类而用接口。接口意味着Can-do关系。如果多种对象类型都能做某事,就为它们创建接口。
例如,一个类型能将自己的实例转换为另一个类型(IConvertible),一个类型能序列化自己的实例(ISerializable)。注意,值类型必须从system.valueType派生,所以不能从一个任意的基类派生。这时必须使用can-do关系并定义接口。
- 易用性
对于开发人员,定义从基类派生的新类型通常比实现接口的所有方法容易得多。基类可提供大量功能,所以派生类型可能只需要稍微改动。而提供接口的话,新类型必须实现所有成员。
- 一致性的实现
无论接口协定订立得有多好,都无法保证所有人百分之百正确实现它。事实上,com颇受该问题之累。而如果为基类型提供良好的默认实现,那么一开始得到的就是能正常工作并经过良好测试的类型。以后根据需要修改就可以了。
- 版本控制
向基类型添加一个方法,派生类将继承新方法。一开始使用就是一个能正常工作的类型,用户的源代码甚至不需要编译。向接口添加一个新成员,会强迫接口的实现者更改其源代码。
最后要指出的是,这两件事情实际上是可以同时做:定义一个接口,同时提供一个实现了这个接口的基类。
FCL中涉及数据流处理的类采用的是实现继承方法。system.IO.Stream是抽象基类,提供了包括read和write在内的一组方法。其他类(filestream,memoryStream和NetWorkStream等)都从stream派生。在这三个类中,每一个和stream类都是is–a关系,这使得具体类的实现变得更容易。
相反,Microsoft采用基于接口的方式设计FCL中的集合。 FCL定义了IComparer<in T>接口,还提供了抽象基类Compare<T>,它实现了该接口,同时为非泛型IComparer的Compare方法提供了默认实现. 接口定义和基类同时存在带来了很大的灵活性.