c#中可调后端泛型的类型安全
本文关键字:泛型 类型安全 后端 | 更新日期: 2023-09-27 18:03:56
考虑编写一个依赖于必须是模块化的后端软件。只要后端适合单个类,通过定义一些接口并让几个后端类实现它很容易实现:
interface IBackend { ... }
class BackendA : IBackend { ... }
class BackendB : IBackend {...}
现在,假设使用后端需要保存中间数据块,其内部依赖于后端。同样,它由接口和具体类表示:
interface IFoo { ... }
class FooA : IFoo { ... }
class FooB : IFoo { ... }
Foo的构造函数没有公开,我们可以依赖IBackend接口中的工厂方法:
interface IBackend {
IFoo createFoo(some arguments)
}
class BackendA : IBackend {
override FooA createFoo(some arguments) {
return new FooA(some arguments)
}
}
}
使用来自公共抽象层的接口无处不在,但现在BackendA的代码在需要时被迫接受IFoo
而不是FooA
,并将其转换为FooA
:
interface IBackend {
void soSomethingWithFoos(IFoo f1, IFoo f2);
}
class BackendA : IBackend {
override void doSomethingWithFoos(IFoo f1, IFoo f2) {
FooA fa1 = (FooA)f1;
FooA fa2 = (FooA)f2;
// do something
}
}
此外,这不是类型安全的,因为粗心的用户可以同时实例化不同的后端,并在编译器的鼻子底下将对象从一个后端传递到另一个后端:
BackendA ba = new BackendA();
BackendB bb = new BackendB();
ba.doSomethingWithFoo(bb.createFoo(some args)); // Typechecks but is clearly incorrect
为了改进这一点,我们可以在所有常用接口中添加类型参数:IBackend<T>
, IFoo<T>
等。并使用它来区分后端:BackendA : IBackend<A>
, BackendB : IBackend<B>
, FooA : IFoo<A>
。然后我们可以输入:
interface IBackend<T> {
public IFoo<T> createFoo(some arguments)
}
这样,编译器不允许混合来自不同后端的对象。然而,这并不十分令人满意:来自某些后端BackendA
的代码仍然必须将IFoo<A>
转换为FooA
,即使只要FooA是唯一实现IFoo<A>
的类,这将是类型安全的。此外,愿意独立于后端的代码现在到处都是泛型。
为了解决这个问题,如果我们假设需要表示后端的类的数量是有限和恒定的,我们可以通过将所有具体类的接口参数化来避免强制类型转换:
interface IBackend<T, FooT, BarT> where FooT : IFoo<T>, BarT : IBar<T> {
FooT createFoo();
...
}
我甚至不知道这是否是有效的语法,也不知道这是否真的解决了问题。
这是疯狂吗?有没有类型安全的方法?或者有没有不使用泛型的更好的方法?
您可以通过稍微颠倒角色来解决这个问题。而不是让IBackend
像这样接受IFoo
,并相信IFoo
总是与相应的IBackend
一起使用,例如
interface IBackend {
IFoo createFoo();
void doSomethingWithFoo(IFoo foo);
}
你可以改变这个,让IFoo
记住它来自哪个IBackend
。
interface IBackend {
IFoo createFoo();
}
interface IFoo {
void doSomethingWithBackend();
}
class BackendA : IBackend {
IFoo createFoo() {
return new FooA(this);
}
void doSomethingWithFoo(FooA foo) { ... }
}
class FooA : IFoo {
private BackendA backend;
FooA(BackendA backend) {
this.backend = backend;
}
void doSomethingWithBackend() {
backend.doSomethingWithFoo(this);
}
}
你想要的是所谓的"虚拟类型",它基本上使类型参数成为虚拟的东西,可以被派生类覆盖。用来促进这个想法的典型用例是当您有一个类型族时,所有这些类型都必须被一致地覆盖。
它们看起来像这样:
class Foo {
virtual type T = Bar;
virtual type R = Baz;
abstract T CreateT() {
}
}
class FooDerived : Foo {
override type T = BarDerived;
}
在foo对象上使用类型成员T时,会看到一个Bar。在FooDerived中你看到BarDerived。这对程序员来说非常方便。
显然c#没有这个特性。
我所知道的唯一支持虚拟类型的语言是Beta。
你可以在这里阅读:
http://en.Wikipedia.org/wiki/BETA 需要注意的重要一点是,这不是静态类型安全的。当编译器支持它时,它需要大量的运行时检查才能真正实现类型安全。最接近的静态类型等价物是变量接口(c#确实支持),但是每个"实体"类型都需要一个类型参数,这将是笨拙的。引用接口会非常嘈杂(IBackend<T1,...,T10>
)。更重要的是,变体形参既可以用作实参,也可以用作返回类型。不是两个。对于您的示例代码,这对您不起作用。
这就说到我的要点了。您想要做的事情本质上是动态类型的。所以,我不会尝试让它是静态类型的。这只会让事情变得比需要的更复杂。试着变得更有活力。这可能会简化后端和实体接口。此外,您的后端界面似乎有点"喋喋不休"。这通常意味着"非最佳抽象"。最好的接口是广泛而简单的,考虑IEnumerable或IDisposable。
你的后端界面应该是这样的:
interface IBackend {
void Run(IFrontEndStuff);
}
如果您的设计应该是模块化的,那么强制转换应该是完全不必要的。强制类型转换(通常)是一个非模块化系统的标志。
为了使事情完全模块化,接口IFoo
应该捕获使用IFoo
实现实例所需的操作。棘手的部分是为你的问题找到一个足够高的抽象。在此之后,提供一个接口来表示该抽象就变得微不足道了。当然,这通常说起来容易做起来难,但是如果您正在寻找完整的类型安全模块化,那么在我看来,这就是您要走的路。
我不确定这是否是您正在寻找的,但一种方法可能是一个抽象基类Foo,然后是一个动态分派到具体处理程序的处理程序。比如:
void FooHandler(dynamic foo)
{
FooHandlerImpl(foo);
}
void FooHandlerImpl(FooA foo)
{
//whatever you do with FooA
}
void FooHandlerImpl(FooB foo)
{
//whatever you do with FooB
}
如何使用离散接口来分离关注点?(松散耦合)
大概是这样的…
class Program
{
static void Main(string[] args)
{
IBackend addon = new FooA();
Console.WriteLine("Enter something if you like");
var more = Console.ReadLine();
var result = Runtime(addon);
Console.WriteLine("Result: {0}", result ?? "No Output :o(");
}
static object Runtime(IBackend addon, string more = null)
{
var need = addon as INeed;
if (need != null)
need.Input = more;
addon.Execute();
var give = addon as IGive;
if (give != null)
return give.Output;
return null;
}
}
public interface IBackend
{
void Execute();
}
public interface INeed
{
string Input { set; }
}
public interface IGive
{
string Output { get; }
}
public class FooA : IBackend, INeed, IGive
{
public void Execute()
{
Console.WriteLine(this.Input ?? "No input :o(");
if (!string.IsNullOrWhiteSpace(this.Input))
this.Output = "Thanks!";
}
public string Input { private get; set; }
public string Output { get; private set; }
}
如果你的附加项/后端项不提供接口,它将被应用程序的那部分忽略。你可以用泛型来加强这一点,但我不确定这真的是必要的。
使用集合和委托,你甚至可以使它更抽象,允许更多的运行时灵活性