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();
    ...
}

我甚至不知道这是否是有效的语法,也不知道这是否真的解决了问题。

这是疯狂吗?有没有类型安全的方法?或者有没有不使用泛型的更好的方法?

c#中可调后端泛型的类型安全

您可以通过稍微颠倒角色来解决这个问题。而不是让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; }
}

如果你的附加项/后端项不提供接口,它将被应用程序的那部分忽略。你可以用泛型来加强这一点,但我不确定这真的是必要的。

使用集合和委托,你甚至可以使它更抽象,允许更多的运行时灵活性