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

深入泛型数组的兔子洞

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.83/5 (29投票s)

2016年5月4日

CPOL

6分钟阅读

viewsIcon

32897

downloadIcon

158

一次关于泛型、反转面向对象编程和泛型类型调度的爱丽丝梦游仙境之旅

问题

最近我有一个奇怪的需求,需要创建一个不同泛型类型的数组,并且能够调用对具体类型进行操作的方法。基本上,我想实现的效果大概是这样的(非工作示例):

public class Cat
{
  public void Meow() {Console.WriteLine("Meow");}
}

public class Dog
{
  public void Bark() {Console.WriteLine("Woof");}
}

public static class BasicExample
{
  static void Test()
  {
    object[] msgs = new object[]
    {
      new Cat(),
      new Dog(),
    };

    DoSomethingWith(msgs[0]);
    DoSomethingWith(msgs[1]);
  }

  static void DoSomethingWith(Cat cat)
  {
    cat.Meow();
  }

  static void DoSomethingWith(Dog dog)
  {
    dog.Bark();
  }
}

请忽略通常实现上述示例的方式是通过一个实现类似 `Speak()` 的公共接口。在我的特定情况下,我需要一个具体类型的数组,它们的属性和方法各不相同,并且我可以调用一个外部定义的方法,该方法对具体类型实例执行一些特定操作,并通过对实例属性的过滤进行限定。

上述代码无效,因为在 `DoSomethingWith(msgs[0]);` 中,参数 `msgs[0]` 的类型是 `object`,而不是数组中初始化的确切类型。(对于高级读者来说,我关于协变和逆变的实验没有得出任何结果。)

所以我想,我们用泛型来实现。我只需要一个泛型数组,像这样:

public class Animal<T> { }
Animal<T>[] msgs = new Animal<T>[]
{
  new Animal<Cat>(),
  new Animal<Dog>(),
};

哎呀,当然这不起作用,因为左值 `Animal[] msgs` 不是已知类型——`T` 必须是已定义的类型。

于是我快速在 Google 上搜索了一下,发现 Stack Overflow 上有一篇有趣的文章(这里),回复是:“不可能。”

这不能接受!

我们进入兔子洞!

解决方案相当简单——我们必须创建一个泛型类型(我们已经创建了 `Animal`),但要让它派生自一个非泛型类型。

public abstract class Animal{ }

public class Animal<T> : Animal { }

现在,数组可以是 `Animal` 类型。

Animal[] animals = new Animal[]
{
  new Animal<Cat>(),
  new Animal<Dog>(),
};

但是 `DoSomethingWith` 调用仍然不正确,因为现在的类型是 `Animal`。

解决这个问题的方法是反转调用,以便 `DoSomethingWith` 由泛型 `Animal` 类调用,因为在那里我们知道 `T` 是什么类型。

这需要一个 `Action`。

public class Animal<T> : Animal 
{
  public Action<T> Action { get; set; }
}

我们现在在构造特定泛型类型时初始化动作。

Animal[] animals = new Animal[]
{
  new Animal<Cat>() {Action = (t) => DoSomethingWith(t)},
  new Animal<Dog>() {Action = (t) => DoSomethingWith(t)},
};

但是我们如何调用这个动作呢?秘密武器在于非泛型 `Animal` 中一个抽象的 `Call` 方法,它在泛型 `Animal` 类中实现。

public abstract class Animal
{
  public abstract void Call(object animal);
}

public class Animal<T> : Animal 
{
  public Action<T> Action { get; set; }

  public override void Call(object animal)
  {
    Action((T)animal);
  }
}

还要注意在调用 `Action` 时对 `T` 的类型转换!

现在,这可以工作了。

public static class BasicConcept3
{
  public static void Test()
  {
    Animal[] animals = new Animal[]
    {
      new Animal<Cat>() {Action = (t) => DoSomethingWith(t)},
      new Animal<Dog>() {Action = (t) => DoSomethingWith(t)},
    };

    animals[0].Call(new Cat());
  }

  static void DoSomethingWith(Cat cat)
  {
    cat.Meow();
  }

  static void DoSomethingWith(Dog dog)
  {
    dog.Bark();
  }
}

这是怎么回事?

 

但是等等,这很蠢!

如果我已经实例化了 `Cat` 和 `Dog`,为什么不直接这样做呢?

DoSomethingWith(new Cat());
DoSomethingWith(new Dog());

因为上一节的代码只是一个示例,用于说明实现。真正的目标是能够通过事件接收一个 `Animal`,例如:

public class SpeakEventArgs : EventArgs
{
  public IAnimal Animal { get; set; }    // Could have been object Animal as well.
}

public static EventHandler<SpeakEventArgs> Speak;

在这里,我们正在创建一个关注点分离——事件接收一些对象,我们将找出如何将该对象“路由”到所需的处理程序。

你会注意到我悄悄地加入了一个接口 `IAnimal`。这在技术上不是必需的,属性 `Animal` 也可以简单是一个对象,但这确保我们使用实现该接口的类型创建 `SpeakEventArgs`。

public class Cat : IAnimal
{
  public void Meow() { Console.WriteLine("Meow"); }
}

public class Dog : IAnimal
{
  public void Bark() { Console.WriteLine("Woof"); }
}

现在,我们的实现需要一种方法来选择“路由”,这意味着我们还必须公开 `T` 的类型,以便限定路由。回到泛型和非泛型类,我们添加一个 `Type` 属性。

public abstract class Animal
{
  public abstract Type Type { get; }
  public abstract void Call(object animal);
}

public class Animal<T> : Animal
{
  public override Type Type { get { return typeof(T); } }
  public Action<T> Action { get; set; }

  public override void Call(object animal)
  {
    Action((T)animal);
  }
}

现在我们可以实现一个调度器。

public static void OnSpeak(object sender, SpeakEventArgs args)
{
  animalRouter.Single(a => args.Animal.GetType() == a.Type).Call(args.Animal);
}

完整的类现在看起来像这样(所有这些 `static` 只是为了方便,没有任何东西阻止你移除 `static` 关键字并实例化类):

public static class EventsConcept
{
  public static EventHandler<SpeakEventArgs> Speak;

  private static Animal[] animalRouter = new Animal[]
  {
    new Animal<Cat>() {Action = (t) => DoSomethingWith(t)},
    new Animal<Dog>() {Action = (t) => DoSomethingWith(t)},
  };

  static EventsConcept()
  {
    Speak += OnSpeak; 
  }

  public static void OnSpeak(object sender, SpeakEventArgs args)
  {
    animalRouter.Single(a => args.Animal.GetType() == a.Type).Call(args.Animal);
  }

  static void DoSomethingWith(Cat cat)
  {
    cat.Meow();
  }

  static void DoSomethingWith(Dog dog)
  {
    dog.Bark();
  }
}

在程序的其他地方,现在可以触发事件。

EventsConcept.EventsConcept.Speak(null, new EventsConcept.SpeakEventArgs() 
{ Animal = new EventsConcept.Cat() });
EventsConcept.EventsConcept.Speak(null, new EventsConcept.SpeakEventArgs() 
{ Animal = new EventsConcept.Dog() });

你又说,但这很蠢!我可以直接这样做:

EventsConcept.EventsConcept.DoSomethingWith(new EventsConcept.Cat());
EventsConcept.EventsConcept.DoSomethingWith(new EventsConcept.Dog());

是的,当然,但这假设动物的*发布者*知道如何处理它。在之前的事件实现中,我们*发布*动物,然后由*订阅者*决定如何处理。

我们在这里所做的就是颠覆了面向对象编程——我们通过调度器,本质上实现了 OOP 幕后的动态调度

一个更好的例子——过滤

我真正需要的是一种根据一个或多个字段的值来调度(即路由)对象的方法。例如:

public class Cat : IAnimal
{
  public bool IsSiamese {get; set;}
}

public class Dog : IAnimal
{
  public bool IsRotweiler {get;set;}
}

请注意,我还删除了 `Speak` 方法,因为我们希望对具体 `Animal` 类型进行“计算”与 `Animal` 实例(以及发布者)解耦。

这需要在我们的泛型和非泛型实例管理类中添加一个 `abstract Where` 方法和一个用于实现过滤表达式的 `Func`。

public abstract class Animal
{
  public abstract Type Type { get; }
  public abstract void Call(object animal);
  public abstract bool Where(object animal);
}

public class Animal<T> : Animal
{
  public override Type Type { get { return typeof(T); } }
  public Action<T> Action { get; set; }
  public Func<T, bool> Filter { get; set; }

  public override void Call(object animal)
  {
    Action((T)animal);
  }

  public override bool Where(object animal)
  {
    return animal is T ? Filter((T)animal) : false;
  }
}

注意,`Where` 的实现也检查了类型——如果我们不这样做,在执行过滤器时会得到运行时错误。

我们的测试类现在看起来像这样:

public static class FilteredEventsConcept
{
  public static EventHandler<SpeakEventArgs> Speak;

  private static Animal[] animalRouter = new Animal[]
  {
    new Animal<Cat>() {Filter = (t) => t.IsSiamese, 
    Action = (t) => Console.WriteLine("Yowl!")},
    new Animal<Cat>() {Filter = (t) => !t.IsSiamese, 
    Action = (t) => Console.WriteLine("Meow")},
    new Animal<Dog>() {Filter = (t) => t.IsRotweiler, 
    Action = (t) => Console.WriteLine("Growl!")},
    new Animal<Dog>() {Filter = (t) => !t.IsRotweiler, 
    Action = (t) => Console.WriteLine("Woof")},
  };

  static FilteredEventsConcept()
  {
    Speak += OnSpeak;
  }

  public static void OnSpeak(object sender, SpeakEventArgs args)
  {
    animalRouter.Single(a => a.Where(args.Animal)).Call(args.Animal);
  }
}

我们的测试像这样:

FilteredEventsConcept.TestCase.Test();

// So we stay in the same namespace and our test is easier to read:
public static class TestCase
{
  public static void Test()
  {
    FilteredEventsConcept.Speak(null, new SpeakEventArgs() { Animal = new Cat() });
    FilteredEventsConcept.Speak(null, new SpeakEventArgs() { Animal = new Cat() { IsSiamese = true } });
    FilteredEventsConcept.Speak(null, new SpeakEventArgs() { Animal = new Dog() });
    FilteredEventsConcept.Speak(null, new SpeakEventArgs() { Animal = new Dog() { IsRotweiler = true } });
  }
}

现在我们已经完成了一些有用的事情——我们的调度器不仅基于类型进行调度,还允许我们通过对类型实例数据的某些过滤来限定操作。所有这些只是为了取代:

if (animal is Cat && ((Cat)animal).IsSiamese) Console.WriteLine("Yowl!");

等等。但是我不喜欢在有争议的、更复杂的声明式解决方案时使用命令式代码!

次要重构

因为这是一个*泛型*路由器,我们应该将支持路由的两个类重命名,并将 `animal` 参数名简单地改为 `obj`。

public abstract class Route
{
  public abstract Type Type { get; }
  public abstract void Call(object obj);
  public abstract bool Where(object obj);
}

public class Route<T> : Route
{
  public override Type Type { get { return typeof(T); } }
  public Action<T> Action { get; set; }
  public Func<T, bool> Filter { get; set; }

  public override void Call(object obj)
  {
    Action((T)obj);
  }

  public override bool Where(object obj)
  {
    return obj is T ? Filter((T)obj) : false;
  }
}

鸭子类型

在 Python 中,我们可以做类似的事情:

class Cat(object):
  def __init__(self):
    self.isSiamese = False

class Dog(object):
  def __init__(self):
    self.isRotweiler = False 

class Route(object):
  def __init__(self, typeCheck, filter, do):
    self.__typeCheck = typeCheck
    self.__filter = filter
    self.__do = do

  def where(self, obj):
    return self.__isType(obj) and self.__filter(obj)

  def do(self, obj):
    self.__do(obj)

  # Attributes and functions with a two leading underscore is the pythonic way of 
  # indicating the method is supposed to be "private", as this "scrambles" the name.
  def __isType(self, obj):
    return self.__typeCheck(obj)

  def __filter(self, obj):
    return self.__filter(obj)

router = [
  Route(lambda animal : type(animal) is Cat, 
  lambda animal : animal.isSiamese, lambda animal : speak("Yowl!")),
  Route(lambda animal : type(animal) is Cat, 
  lambda animal : not animal.isSiamese, lambda animal : speak("Meow")),
  Route(lambda animal : type(animal) is Dog, 
  lambda animal : animal.isRotweiler, lambda animal : speak("Growl!")),
  Route(lambda animal : type(animal) is Dog, 
  lambda animal : not animal.isRotweiler, lambda animal : speak("Woof"))
]
def speak(say):
  print(say)

def dispatcher(animal):
  filter(lambda route : route.where(animal), router)[0].do(animal)

cat1 = Cat()
cat2 = Cat()
cat2.isSiamese = True

dog1 = Dog()
dog2 = Dog()
dog2.isRotweiler = True

dispatcher(cat1)
dispatcher(cat2)
dispatcher(dog1)
dispatcher(dog2)

Python 代码的重点是这个:

router = [
  Route(lambda animal : type(animal) is Cat, 
	lambda animal : animal.isSiamese, lambda animal : speak("Yowl!")),
  Route(lambda animal : type(animal) is Cat, 
	lambda animal : not animal.isSiamese, lambda animal : speak("Meow")),
  Route(lambda animal : type(animal) is Dog, 
	lambda animal : animal.isRotweiler, lambda animal : speak("Growl!")),
  Route(lambda animal : type(animal) is Dog, 
	lambda animal : not animal.isRotweiler, lambda animal : speak("Woof"))
]

在这里,lambda 表达式 `lambda animal : animal.isSiamese` 是*鸭子类型*的。它不需要在“编译时”(因为没有编译时)知道类型,就能评估 lambda 表达式。相反,在 C# 中,IntelliSense 已经知道类型,因为 `t` 只能是 `Cat` 类型。

在 Python 中,唯一能知道你没有搞砸的方法

Route(lambda animal : type(animal) is Cat, 
lambda animal : animal.isRotweiler, lambda animal : speak("Yowl!")),

是运行(或最好编写单元测试)代码。

C# 鸭子类型

从技术上讲,我们的 C# 代码也存在同样的问题。考虑这个更简单的例子:

public abstract class SomeMessage
{
  public abstract void Call(object withMessage);
}

public class Message<T> : SomeMessage
{
  public Action<T> Action { get; set; }

  public override void Call(object withMessage)
  {
    Action((T)withMessage);
  }
}

public class CallbackExamplesProgram
{
  public static void Test()
  {
    SomeMessage[] messages = new SomeMessage[]
    {
      new Message<MessageA>() {Action = (msg) => MessageCallback(msg)},
      new Message<MessageB>() {Action = (msg) => MessageCallback(msg)}
    };

  try
  {
    // Cast error caught at runtime:
    messages[0].Call(new MessageB());
  }
  catch (Exception ex)
  {
    Console.WriteLine(ex.Message);
  }
}

在这里,我们为第一个 `MessageA` 类型的消息调用 `Action`,但传入了一个 `MessageB` 实例。我们在运行时(而不是编译时!)得到:

这就是为什么在前面的过滤事件示例中,`Where` 方法会检查类型。

public override bool Where(object obj)
{
  return obj is T ? Filter((T)obj) : false;
}

访问者模式

正如 ND Hung 在评论中指出的那样,访问者模式是解决此问题的好 OOP 解决方案。一个最简单的例子可能如下所示:

namespace VisitorPatternExample
{
  public interface IVisitor
  {
    void Visit(Cat animal);
    void Visit(Dog animal);
  }

  public interface IAnimal
  {
    void Accept(IVisitor visitor);
  }

  public class Cat : IAnimal
  {
    public void Accept(IVisitor visitor)
    {
      visitor.Visit(this);
    }
  }

  public class Dog : IAnimal
  {
    public void Accept(IVisitor visitor)
    {
      visitor.Visit(this);
    }
  }

  public class Visitor : IVisitor
  {
    public void Visit(Cat cat)
    {
      Console.WriteLine("Cat");
    }

    public void Visit(Dog dog)
    {
      Console.WriteLine("Dog");
    }
  }

  public static class VisitorTest
  {
    public static void Test()
    {
      IAnimal cat = new Cat();
      IAnimal dog = new Dog();

      Visitor visitor = new Visitor();
      cat.Accept(visitor);
      dog.Accept(visitor);
    }
  }
}

声明式 vs. 命令式,以及用例分析

访问者模式避免了泛型数组的整个问题,这正是我要解决的目标。然而,这表明我们可能认为*用例*(至少在我的示例中)需要一个泛型数组,但实际上,像访问者模式这样简单的东西可以解决问题,而无需颠覆 OOP 并创建复杂的泛型数组解决方案。它还揭示了声明式与命令式代码之间的差异(以及由此产生的复杂性)。使用访问者模式,过滤必须以命令式方式实现:

public void Visit(Cat cat)
{
  if (cat.IsSiamese)
    Console.WriteLine("Yowl!");
  else
    Console.WriteLine("Meow.");
}

public void Visit(Dog dog)
{
  if (dog.IsRotweiler)
    Console.WriteLine("Growl!");
  else
    Console.WriteLine("Woof");
  }
}

这当然是一个完全可接受的解决方案。反之,对于声明式解决方案,我们不需要 `IVisitor`(或类似)接口,因此当添加新消息类型时,只需更改声明式路由器。在访问者模式中,必须修改接口以添加新类型的 `Visit` 方法。权衡在于实现声明式方法的复杂性与命令式方法的简洁性。

结论

这最初打算作为一条提示发布,但篇幅有点太长了!访问者模式并没有演示如何使用泛型数组(这是本文的重点),但它很好地说明了如何使用设计模式来解决用例(毕竟它不需要泛型数组),以及命令式和声明式编程之间的权衡。

源代码包含本文中的示例以及更多内容。

© . All rights reserved.