深入泛型数组的兔子洞






4.83/5 (29投票s)
一次关于泛型、反转面向对象编程和泛型类型调度的爱丽丝梦游仙境之旅
问题
最近我有一个奇怪的需求,需要创建一个不同泛型类型的数组,并且能够调用对具体类型进行操作的方法。基本上,我想实现的效果大概是这样的(非工作示例):
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
于是我快速在 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
这需要一个 `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` 方法。权衡在于实现声明式方法的复杂性与命令式方法的简洁性。
结论
这最初打算作为一条提示发布,但篇幅有点太长了!访问者模式并没有演示如何使用泛型数组(这是本文的重点),但它很好地说明了如何使用设计模式来解决用例(毕竟它不需要泛型数组),以及命令式和声明式编程之间的权衡。
源代码包含本文中的示例以及更多内容。