另一种多态方式






4.09/5 (8投票s)
2005 年 2 月 9 日
9分钟阅读

53162

71
一篇关于如何在 C# 中模拟即席适配器生成的文章。
引言
本文将多态视为“以通用方式与不同类的对象进行交互的能力”。下面将讨论*即席可适配(AHA)*多态。详细介绍了启用 C# 编程语言*AHA 多态*的库。
背景
总的来说,在我了解的语言中,存在以下几种多态:
- 经典多态,使用继承和虚方法,就像大多数静态类型面向对象语言一样;
- 不安全的运行时*即席*多态,就像不同的脚本语言一样;
- 安全的编译时多态,就像 C++ 一样。
例如,假设您有一个高级的第三方Com.Example.ListBox
控件。它在许多功能上都不同于标准的System.Windows.Forms.ListBox
。这个ListBox
的独特性在于它使用ListBox.Item
接口来接收项的名称。也许这是因为该控件的开发人员忘记了Object.ToString()
方法。或者也许他们希望在不久的将来引入Item.Color
属性。
此外,您还有两个类:Rocket
和Smile
。它们是完全独立的。而且,这两个类都不是设计为在ListBox
控件中显示的。Rocket
用于飞行,Smile
用于微笑。它们都是由不同的程序员团队在两年前设计和实现的。但现在,出于某些业务原因,您需要创建一个包含火箭和微笑交替显示的ListBox
。
碰巧,这两个类都已经有了Name
属性。
解决此问题的最简单方法是将这些类继承自ListBox.Item
接口。主要缺点是会产生不必要的耦合。请记住,我们的类最初根本不打算列在ListBox
中。
下面列表中的 C# 代码显示了此解决方案。请注意,Rocket
和Smile
类通过ListBox.Item
接口相互连接。
public class ListBox
{
public interface Item
{
string Name {get;}
}
public void Add(Item item)
{
string name = item.Name;
Console.Out.WriteLine(name);
//...
}
}
public class Rocket : ListBox.Item
{
private string name;
public Rocket(string name)
{
this.name = name;
}
public string Name
{
get { return name; }
}
}
public class Smile : ListBox.Item
{
private string name;
public Smile(string name)
{
this.name = name;
}
public string Name
{
get { return name; }
}
}
public static void Test()
{
ListBox box = new ListBox();
Rocket rocket = new Rocket("progress");
Smile smile = new Smile("gigling");
box.Add(rocket);
box.Add(smile);
}
即席方法是在获取对象之前检查对象是否具有Name
属性。类保持未连接。但这种方法不安全,并且会导致维护噩梦。每次添加新类或更改现有类时,您都需要手动检查Name
属性是否存在。否则,将在运行时产生意外错误。收到“看起来您忘记在Rocket
类中实现Name
属性”的客户错误报告会很有趣。
编译时方法是将ListBox.Add
方法定义为模板方法。这样,将在编译时为要添加的每个对象检查Name
属性是否存在。如果Name
不存在,则会生成编译错误。此方法的主要缺点是您的部分代码将变成*静态*。例如,无法定义具有Add()
模板方法的IListBox
接口,因为模板方法不能是虚的。
可适配多态
还有一种方法,甚至很少在文献中提及。其思想是从每个类提供一个到Item
接口的适配器。这样,类仍然保持未连接。这是安全的,因为当添加新类时,不可能忘记创建新的适配器。您只会收到编译错误,这比运行时错误要好得多。
*可适配多态*是高效的,因为它只需要一次额外的函数调用。它比*即席多态*快,但比编译时多态稍慢。
上一个示例的可适配代码如下所示。
public class ListBox
{
public interface Item
{
string Name {get;}
}
public void Add(Item item)
{
string name = item.Name;
Console.Out.WriteLine(name);
//...
}
}
public class Rocket
{
private string name;
public Rocket(string name)
{
this.name = name;
}
public string Name
{
get { return name; }
}
}
public class Smile
{
private string name;
public Smile(string name)
{
this.name = name;
}
public string Name
{
get { return name; }
}
}
public class RocketItemAdapter : ListBox.Item
{
private Rocket handler;
public RocketItemAdapter(Rocket rocket)
{
this.handler = rocket;
}
public string Name
{
get
{
return handler.Name;
}
}
}
public class SmileItemAdapter : ListBox.Item
{
private Smile handler;
public SmileItemAdapter(Smile handler)
{
this.handler = handler;
}
public string Name
{
get
{
return handler.Name;
}
}
}
public static void Test()
{
ListBox box = new ListBox();
Rocket rocket = new Rocket("progress");
Smile smile = new Smile("gigling");
box.Add(new RocketItemAdapter(rocket));
box.Add(new SmileItemAdapter(smile));
}
此方法的主要限制是程序员很懒惰。编写大量适配器很繁琐。假设Item
包含十二个方法,而您有一百个可以列在ListBox
中的类。
可以在 C++ 中创建适配器类模板,但即便如此,Item
惯用法至少也会加倍(在Item
本身以及适配器模板类中)。当然,这比为 C# 中的每个类编写适配器要好。
adapt 关键字
问题是为什么编程语言不提供*即席适配*功能。我对此感到非常好奇。解决方案非常简单。请看下面*无法编译*的代码。
public class ListBox
{
public interface Item
{
string Name {get;}
}
public void Add(Item item)
{
string name = item.Name;
Console.Out.WriteLine(name);
//...
}
}
public class Rocket
{
private string name;
public Rocket(string name)
{
this.name = name;
}
public string Name
{
get { return name; }
}
}
public class Smile
{
private string name;
public Smile(string name)
{
this.name = name;
}
public string Name
{
get { return name; }
}
}
public static void Test()
{
ListBox box = new ListBox();
Rocket rocket = new Rocket("progress");
Smile smile = new Smile("gigling");
box.Add(adapt<ListBox.Item>(rocket));
box.Add(adapt<ListBox.Item>(smile));
}
我只希望编译器能够自动隐式地在编译时创建RocketItemAdapter
和SmileItemAdapter
类。这对编译器来说很容易,因为所有信息都存在。毕竟,此功能比 C++ 风格的模板简单得多。
就是这样。只需要求您的编译器供应商改进您钟爱的语言。如果供应商是个好人,那么您最早可以在下周就使用adapt
关键字。
下面将讨论在 C# v.1 语言中模拟adapt
关键字,以确保这一点。我相信没有丑陋而迟钝的编译器供应商。
AHA 库
AHA 库的主要思想是在运行时发出适配器。使用System.Reflection.Emit
命名空间工具可以轻松实现这一点。主要难点在于以类型安全的方式实现。很容易陷入不安全的*即席多态*。我希望在*运行时*无法发出适配器时获得*编译时*错误。
这本身就是一个有趣的挑战。而且,它似乎是无法实现的。因此,AHA 库在*初始化时*产生错误。这比编译时差,但比运行时好得多。您负责在发布程序之前运行一次程序。如果您的程序中*任何地方*存在无效的适配请求,您的程序将在启动后立即崩溃。
长话短说,看看下面的代码。
public class ListBox
{
public interface Item
{
string Name {get;}
}
public void Add(Item item)
{
string name = item.Name;
Console.Out.WriteLine(name);
//...
}
}
public class Rocket
{
private string name;
public Rocket(string name)
{
this.name = name;
}
public string Name
{
get { return name; }
}
}
public class Smile
{
private string name;
public Smile(string name)
{
this.name = name;
}
public string Name
{
get { return name; }
}
}
[Adaptation(typeof(ListBox.Item), typeof(Rocket))]
public class RocketItemRequest : AdaptationRequest
{
private Rocket rocket;
public RocketItemRequest(Rocket rocket)
{
this.rocket = rocket;
}
public override object Handler
{
get
{
return rocket;
}
}
}
[Adaptation(typeof(ListBox.Item), typeof(Smile))]
public class SmileItemRequest : AdaptationRequest
{
private Smile smile;
public SmileItemRequest(Smile smile)
{
this.smile = smile;
}
public override object Handler
{
get
{
return smile;
}
}
}
private static void Test()
{
ListBox box = new ListBox();
Rocket rocket = new Rocket("progress");
Smile smile = new Smile("gigling");
box.Add((ListBox.Item)AdapterKit.Adapt(new RocketItemRequest(rocket)));
box.Add((ListBox.Item)AdapterKit.Adapt(new SmileItemRequest(smile)));
}
[STAThread]
static void Main(string[] args)
{
AdapterKit.Anchor();
Test();
}
上面的程序中有以下有趣的点:
RocketItemRequest
类提供了从Rocket
到Item
生成适配器的完整规范。首先,接口和处理程序列在Adaptation
属性中。其次,此请求的可适配对象受限于Rocket
类及其所有后代,通过构造函数签名。- 适配器是使用
AdapterKit.Adapt()
静态方法创建的。 - 一致性检查是通过调用
AdapterKit.Anchor()
静态方法来请求的。
讽刺的是,RocketItemRequest
与手动编写的RocketItemAdapter
一样长。但是RocketItemRequest
的大小是恒定的,并且不依赖于Item
接口的大小。当向Item
添加新方法时,请求的大小不会增加。此外,所有请求都具有相似的结构,并且可以由您的 IDE 动态生成。
一致性检查如何工作
在一致性检查期间,使用Assembly.GetTypes()
方法查找所有适配请求。分析每个请求的Adaptation
属性,库会尝试创建适当的适配器。如果适配失败,程序将立即因异常而失败。创建的适配器会被缓存以备将来使用(也许这是一种过早的优化)。
因此,没有配套的一致性检查工具、构建后步骤或其他配置麻烦。您的程序默认保持一致。
要启用一致性检查,您需要手动调用AdapterKit.Anchor()
方法。但是很难忘记,因为 AHA 库没有 Anchoring 就无法工作。假设当编写第一个适配请求时,程序员出于好奇至少会看它是如何工作的。
AHA 性能
第一个问题是它有多快。答案是“它尽可能快”。这是因为为接口中的每个方法都生成了纯 IL 代码。它和手动编写每个类的适配器一样快。
AhaSpeedTest
示例提供了以下结果:
Handler is directly inherited from the interface
Elapsed time: 00:00:02.5436576
Handler is wrapped by manually written adapter
Elapsed time: 00:00:02.9141904
Handler is wrapped by automatically emitted adapter
Elapsed time: 00:00:02.7139024
正如您所见,自动包装版本甚至比手动版本更快。这是因为使用了OpCodes.Call
而不是OpCodes.Callvirt
。C# 编译器不知道处理程序的具体类型,因此被迫虚拟调用方法。AHA 库在发出适配器时知道处理程序的具体类型,并且可以避免虚拟调用。但这个选择导致我们生成了更多的适配器。
它与泛型有何不同
也许,我不理解泛型。也许,它们的描述不正确。也许,它们在我的 C# v.2 编译器测试版中实现不正确。但它们看起来设计得不正确。因为它们没有为讨论的问题提供解决方案(就像 C++ 模板那样)。
我无法在 C# 2.0 中写出比以下代码更好的代码:
public class ListBox
{
public interface Item
{
string Name {get;}
}
public void Add<T>(T item) where T : Item
{
string name = item.Name;
Console.Out.WriteLine(name);
//...
}
}
public class Rocket : ListBox.Item
{
private string name;
public Rocket(string name)
{
this.name = name;
}
public string Name
{
get { return name; }
}
}
public class Smile : ListBox.Item
{
private string name;
public Smile(string name)
{
this.name = name;
}
public string Name
{
get { return name; }
}
}
public static void Test()
{
ListBox box = new ListBox();
Rocket rocket = new Rocket("progress");
Smile smile = new Smile("gigling");
box.Add(rocket);
box.Add(smile);
}
请注意,Rocket
和Smile
必须继承自Item
接口。并且必须提供where T: Item
约束。因此,*泛型代码*并不比本文中的第一个示例更好。Rocket
和Smile
类仍然相互连接,并且必须在考虑ListBox.Item
接口的情况下进行设计和实现。
与即席多态的主要区别
还有其他框架可以动态构建适配器。
使用 NMock 库很容易引入此功能。有一个 AutoCaster 类提供了类似的功能。
所提方法的主要区别在于改进的安全性。例如,让我们考虑对 AutoCaster 附带示例的修改(请参阅完整的原始代码 此处)。
注意:此代码仅在第二个版本的 C# 中有效。.
/// <summary>
/// Three necessary methods are missed
/// </summary>
public class WrongTest
{
public void DoIt()
{
Console.WriteLine("yeah");
}
}
class TestMain
{
public static void Main()
{
try
{
Test test = new Test();
ITest itest = null;
if (DateTime.Now.DayOfWeek == DayOfWeek.Sunday)
//the problem is here, but it's visible only on Sundays
itest = Latent<ITest>.Cast(new WrongTest());
else
itest = Latent<ITest>.Cast(test);
itest.DoIt();
itest.Print("Hello", "World");
itest.Print(1, 2);
Console.WriteLine(itest.Sum(1, 2));
}
catch (Exception e)
{
Console.WriteLine(e);
Console.WriteLine(e.StackTrace);
}
}
}
上述代码在周日无效。但在其他任何一周的任何一天都有效。因此,非常难以找到此问题。
AHA 的类似示例虽然更冗长,但更安全。仅仅因为周日问题在每周的其他任何一天都可见。
public interface ITest
{
void DoIt();
}
public class Test
{
public void DoIt()
{
Console.Out.WriteLine("It's done!");
}
}
//this is a 'problematic class'
public class WrongTest
{
public void WrongDoing()
{
Console.Out.WriteLine("It's cannot be done!");
}
}
[Adaptation(typeof(ITest), typeof(Test))]
public class TestRequest : AdaptationRequest
{
//...
}
[Adaptation(typeof(ITest), typeof(WrongTest))]
public class WrongTestRequest : AdaptationRequest
{
//...
}
[STAThread]
static void Main(string[] args)
{
AdapterKit.Anchor();
ITest itest = null;
if (DateTime.Now.DayOfWeek == DayOfWeek.Sunday)
//the problem is here, and it's visible always
itest =
(ITest)AdapterKit.Adapt(new WrongTestRequest(new WrongTest()));
else
itest = (ITest)AdapterKit.Adapt(new TestRequest(new Test()));
itest.DoIt();
}
尽管冗长,但此功能似乎非常重要。此功能的实现简单且透明。
关于托管 C++
此技术也适用于托管 C++。而且,对于 C++,有更好的解决方案。无需显式适配请求和 Anchoring。可以利用 C++ 模板使其类型安全。下面显示了一个简短的托管 C++ 代码示例。
IFoo* foo = AdapterKit<IFoo, Handler>::Adapt(new Handler());
一致性检查可以在AdapterKit<IFoo, Handler>
的静态字段构造函数中完成。
但是 AHA 的托管 C++ 版本尚未发布。C++/CLI 仍然不可用,托管 C++ 仍然不可用。
如何获取 AHA 库
AHA 库是免费的,可以从 www.vistal.sf.net 下载。现在该库是 ViStaL 项目的一部分。
未来方向
AHA 已计划以下内容:
- 用户偏好(速度或大小)的选择。可以通过使用或不使用适配器中的
OpCodes.Callvirt
来实现这些目标。 - 在硬盘上缓存生成的适配器。适配请求集通常在执行会话之间不会改变,可以缓存。
- 构建后一致性检查工具,当由于部署过程难以运行程序时可能很有用。
- 良好的名称解析算法。
参考文献
一些想法和大量的代码是从 NMock 库中借用的。