65.9K
CodeProject 正在变化。 阅读更多。
Home

另一种多态方式

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.09/5 (8投票s)

2005 年 2 月 9 日

9分钟阅读

viewsIcon

53162

downloadIcon

71

一篇关于如何在 C# 中模拟即席适配器生成的文章。

引言

本文将多态视为“以通用方式与不同类的对象进行交互的能力”。下面将讨论*即席可适配(AHA)*多态。详细介绍了启用 C# 编程语言*AHA 多态*的库。

背景

总的来说,在我了解的语言中,存在以下几种多态:

  • 经典多态,使用继承和虚方法,就像大多数静态类型面向对象语言一样;
  • 不安全的运行时*即席*多态,就像不同的脚本语言一样;
  • 安全的编译时多态,就像 C++ 一样。

例如,假设您有一个高级的第三方Com.Example.ListBox控件。它在许多功能上都不同于标准的System.Windows.Forms.ListBox。这个ListBox的独特性在于它使用ListBox.Item接口来接收项的名称。也许这是因为该控件的开发人员忘记了Object.ToString()方法。或者也许他们希望在不久的将来引入Item.Color属性。

此外,您还有两个类:RocketSmile。它们是完全独立的。而且,这两个类都不是设计为在ListBox控件中显示的。Rocket用于飞行,Smile用于微笑。它们都是由不同的程序员团队在两年前设计和实现的。但现在,出于某些业务原因,您需要创建一个包含火箭和微笑交替显示的ListBox

碰巧,这两个类都已经有了Name属性。

解决此问题的最简单方法是将这些类继承自ListBox.Item接口。主要缺点是会产生不必要的耦合。请记住,我们的类最初根本不打算列在ListBox中。

下面列表中的 C# 代码显示了此解决方案。请注意,RocketSmile类通过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));
}

我只希望编译器能够自动隐式地在编译时创建RocketItemAdapterSmileItemAdapter类。这对编译器来说很容易,因为所有信息都存在。毕竟,此功能比 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类提供了从RocketItem生成适配器的完整规范。首先,接口和处理程序列在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);
}

请注意,RocketSmile必须继承自Item接口。并且必须提供where T: Item约束。因此,*泛型代码*并不比本文中的第一个示例更好。RocketSmile类仍然相互连接,并且必须在考虑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 库中借用的。

© . All rights reserved.