鸟语天空
C#接口
post by:追风剑情 2021-1-29 9:33

接口对一组方法签名进行了统一命名。接口允许定义事件、无参属性、有参属性(C#的索引器)。所有这些东西本质上都是方法,它们只是语法上的简化。不过,接口不能定义任何构造器方法,也不能定义任何实例字段。

虽然CLR允许接口定义静态方法、静态字段、常量和静态构造器,但符合CLS标准的接口绝不允许,因为有的编程语言不能定义或访问它们。事实上,C#禁止接口定义任何一种这样的静态成员。

FCL中的几个接口定义
public interface IDisposable {
	void Dispose();
}

public interface IEnumerable {
	IEnumerator GetEnumerator();
}

public interface IEnumerable<T> : IEnumerable {
	new IEnumerator<T> GetEnumerator();
}

public interface ICollection<T> : IEnumerable<T>, IEnumerable {
	void Add(T item);
	void Clear();
	Boolean Contains(T item);
	void CopyTo(T[] array, Int32 arrayIndex);
	Boolean Remove(T item);
	Int32 Count { get; }
	Boolean IsReadOnly { get; }
}

public interface IComparable<in T> {
	Int32 CompareTo(T other);
}

在CLR看来,接口定义就是类型定义。也就是说,CLR会为接口类型对象定义内部数据结构,同时可通过反射机制来查询接口类型的功能。和类型一样,接口可在文件范围中定义,也可嵌套在另一个类型中。定义接口类型时,可指定你希望在任何可见性/可访问性(public, protected, internal等)。

根据约定,接口类型名称以大写字母I开头,目的是方便在源代码中辨认接口类型。CLR支持泛型接口和接口中的泛型方法。

示例
using System;

// Point 从 System.Object 派生,并实现了 IComparable<T>
public sealed class Point : IComparable<Point> {
	private Int32 m_x, m_y;
	
	public Point(Int32 x, Int32 y) {
		m_x = x;
		m_y = y;
	}

	// 该方法为 Point 实现 IComparable<T>.CompareTo()
	public Int32 CompareTo(Point other) {
		return Math.Sign(Math.Sqrt(m_x * m_x + m_y * m_y)
			- Math.Sqrt(other.m_x * other.m_x + other.m_y * other.m_y));
	}
	
	public override String ToString() {
		return String.Format("({0}, {1})", m_x, m_y);
	}
}

public static class Program {
	public static void Main() {
		Point[] points = new Point[] {
			new Point(3, 3),
			new Point(1, 2),
		};

		// 下面调用由 Point 实现的 IComparable<T>的 CompareTo 方法
		if (points[0].CompareTo(points[1]) > 0) {
			Point tempPoint = points[0];
			points[0] = points[1];
			points[1] = tempPoint;
		}
		Console.WriteLine("Points from closest to (0, 0) to farthest:");
		foreach (Point p in points)
			Console.WriteLine(p);
	}
}

C#编译器要求将实现的接口方法标记为public。CLR要求接口方法标记为virtual。不将方法显式标记为virtual,编译器会将它们标记为virtual和sealed;这会阻止派生类重写接口方法。将方法显式标记为virtual,编译器就会将该方法标记为virtual(并保持它的非密封状态),使派生类能重写它。

派生类不能重写sealed的接口方法。但派生类可重新继承同一个接口,并为接口方法提供自己的实现。在对象上调用接口方法时,调用的是该方法在该对象的类型中的实现。下例对此进行了演示:

示例
using System;

public static class Program {
	public static void Main() {
		/****************** 第一个例子 ******************/
		Base b = new Base(); 
		// 用b的类型来调用Dispose,显示; "Base's Dispose"
		b.Dispose();
		// 用b的对象的类型来调用Dispose,显示; "Base's Dispose"
		((IDisposable)b).Dispose(); 

		/****************** 第二个例子 ******************/
		Derived d = new Derived(); 
		// 用d的类型来调用Dispose,显示; "Derived's Dispose"
		d.Dispose();
		// 用d的对象的类型来调用Dispose,显示; "Derived's Dispose"
		((IDisposable)d).Dispose();

		/****************** 第三个例子 ******************/
		b = new Derived();
		// 用b的类型来调用Dispose,显示; "Base's Dispose"
		b.Dispose();
		// 用b的对象的类型来调用Dispose,显示; "Derived's Dispose"
		((IDisposable)b).Dispose();
	}

	// 这个类派生自Object,它实现了IDisposable
	internal class Base : IDisposable {
		// 这个方法隐式密封,不能被重写
		public void Dispose() {
			Console.WriteLine("Base's Dispose");
		}
	}

	// 这个类派生自Base,它重新实现了IDisposable
	internal class Derived : Base, IDisposable {
		// 这个方法不能重写Base的Dispose,
		// 'new' 表明该方法重新实现了IDisposable的Dispose方法
		new public void Dispose() {
			Console.WriteLine("Derived's Dispose");
			// 注意,下面这行代码展示了如何调用基类的实现(如果需要的话) 
			// base.Dispose();
		}
	}
}

关于调用接口方法的更多探讨

FCL的System.String类型继承了System.Object的方法签名及其实现。此外,String类型还实现了几个接口:IComparable, ICloneable, IConvertible, IEnumerable, IComparable<String>, IEnumerable<Char>IEquatable<String>。这意味着String类型不需要实现(或重写)其Object基类型提供的方法,但必须实现所有接口声明的方法。

CLR允许定义接口类型的字段、参数或局部变量。使用接口类型的变量可以调用该接口定义的方法。此外,CLR允许调用Object定义的方法,因为所有类都继承了Object的方法。以下代码对此进行了演示:

示例
// s变量引用一个String对象
String s = "Jeffrey";
// 可以使用s调用在String,Object,IComparable,ICloneable,
// IConvertible,IEnumerable中定义的任何方法

// cloneable变量引用同一个String对象
ICloneable cloneable = s;
// 使用cloneable只能调用ICloneable接口声明的
// 任何方法(或Object定义的任何方法)

// comparable变量引用同一个String对象
IComparable comparable = s;
// 使用comparable只能调用IComparable接口声明的
// 任何方法(或Object定义的任何方法)

// enumerable变量引用同一个String对象
// 可在运行时将变量从一个接口转换成另一个,只要
// 对象的类型实现了这两个接口
IEnumerable enumerable = (IEnumerable) comparable;
// 使用enumerable只能调用IEnumerable接口声明的
// 任何方法(或Object定义的任何方法)

注意  和引用类型相似,值类型可实现零个或多个接口。但值类型的实例在转换为接口类型时必须装箱。这是由于接口变量是引用,必须指向堆上的对象,使CLR 能检查对象的类型对象指针,从而判断对象的确切类型。调用已装箱值类型的接口方法时,CLR 会跟随对象的类型对象指针找到类型对象的方法表,从而调用正确的方法。

隐式和显式接口方法实现

显式接口方法实现(Explicit Interface Method Implementation, EIMI)

示例
internal sealed class SimpleType : IDisposable {
	public void Dispose() { Console.Writeline("Dispose"); }
}

public sealed class Program {
	public static void Main() {
		SimpleType st = new SimpleType();
		//调用公共Dispose方法实现
		st.Dispose();
		
		//调用IDisposable的Dispose方法的实现
		IDisposable d = st;
		d.Disapose();
	}
}
以上代码执行结果为:
Dispose
Dispose

示例
internal sealed class SimpleType : IDisposable {
	public void Dispose() { Console.Writeline("public Dispose"); }
	//显示实现接口,不允许指定可访问性(比如public或private)。
	//但是,编译器生成方法的元数据时,可访问性会自动设为private,
	//防止其他代码在使用类的实例时直接调用接口方法。
	//只有通过接口类型的变量才能调用接口方法。
	void IDisposable.Dispose() { Console.Writeline("IDisposable Dispose"); }
}

public sealed class Program {
	public static void Main() {
		SimpleType st = new SimpleType();
		//调用公共Dispose方法实现
		st.Dispose();
		
		//调用IDisposable的Dispose方法的实现
		IDisposable d = st;
		d.Disapose();
	}
}
以上代码执行结果为:
public Dispose
IDisposable Dispose

泛型接口

示例
using System;

// 该类型实现泛型IComparable<T>接口两次
public sealed class Number : IComparable<Int32>, IComparable<String> {
	private Int32 m_val = 5;
	
	// 该方法实现IComparable<Int32>的CompareTo方法
	public Int32 CompareTo(Int32 n) {
		return m_val.CompareTo(n);
	}
	
	// 该方法实现IComparable<String>的CompareTo方法
	public Int32 CompareTo(String s) {
		return m_val.CompareTo(Int32.Parse(s));
	}
}

public static class Program {
	public static void Main() {
		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");
	}
}

示例——协变与逆变
// 逆变:指可以传递给定类型的子类作为实参(即, T的子类)。
// 泛型接口的逆变需要关键字in实现
public interface IComparable<in T> {
	int CompareTo(T other);
}

// 协变:指可以返回给定类型的基类(即, T的基类)。
// 泛型接口的协变需要关键字out实现
public interface IEnumerable<out T> : IEnumerable {
	IEnumerator<T> GetEnumerator();
}

//.NET 4.0之后支持协变与逆变,并且尽量使用此特性以增加接口的灵活性。

泛型和接口约束

• 泛型约束可以实现类型安全
• 传递值类型时减少装箱

示例
// M的类型参数T被约束为只支持同时实现了
// IComparable和IConvertible接口的类型
private static Int32 M<T>(T t) where T : IComparable, IConvertible { }
// 这种写法会产生装箱操作
private static Int32 M(IComparable t) { }

C#编译器为接口约束生成特殊IL指令,导致直接在值类型上调用接口方法而不装箱。不用接口约束便没有其他办法让C#编译器生成这些IL指令,如此一来,在值类型上调用接口方法总是发生装箱。一个例外是如果值类型实现了一个接口方法,在值类型的实例上调用这个方法不造成值类型的实例装箱。

实现多个具有相同方法名和签名的接口

示例
public interface IWindow {
	Object GetMenu();
}

public interface IRestaurant {
	Object GetMenu();
}

public sealed class MarioPizzeria : IWindow, IRestaurant {
	Object IWindow.GetMenu() { }
	Object IRestaurant.GetMenu() { }
	//这个GetMenu方法是可选的,与接口无关
	public Object GetMenu() { }
}

MarioPizzeria mp = new MarioPizzeria();
//调用GetMenu()
mp.GetMenu();

//调用IWindow.GetMenu()
IWindow window = mp;
window.GetMenu();

//调用IRestaurant.GetMenu()
IRestaurant restaurant = mp;
restaurant.GetMenu();

显式实现的接口无法被派生类调用

评论:
发表评论:
昵称

邮件地址 (选填)

个人主页 (选填)

内容