繁体   English   中英

使用泛型为C#中的可调后端键入安全性?

[英]Type safety with generics for modulable backends in C# ?

考虑编写一个依赖于后端的软件,该后端必须是模块化的。 只要后端适合单个类,通过定义一些接口并让几个后端类实现它很容易实现:

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

如果您的设计应该按照您的描述进行模块化,则演员应该完全没必要。 演员(通常)是低于模块化系统的标志。

为了使事物完全模块化,接口IFoo应捕获使用IFoo实现实例所必需的操作。 棘手的部分是为您的问题找到足够高的抽象。 在那之后,提供一个表示该抽象的接口变得微不足道。 当然,这通常说起来容易做起来难,但如果你正在寻找完全类型安全的模块化,那么我认为这是最佳选择。

你想要的是一种叫做“虚拟类型”的东西,它基本上使类型参数成为可以被派生类覆盖的虚拟东西。 用于推广这一想法的典型用例是当您拥有一系列类型时,所有类型都必须一致地覆盖。

他们看起来像这样:

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> )。 更重要的是,variant参数既可以用作参数,也可以用作返回类型。 不是都。 鉴于您的示例代码,这对您不起作用。

这是我的主要观点。 你想要做的是固有的动态类型。 所以,我不会尝试静态输入。 这只是让事情变得比他们需要的更复杂。 尝试更有活力。 这可能会简化后端和实体接口。 此外,您的后端界面似乎有点“健谈”。 这通常表示“非最佳抽象”。 最好的界面是广泛而简单的想想IEnumerable或IDisposable。

理想情况下,您的后端界面应如下所示:

interface IBackend {
    void Run(IFrontEndStuff);
}

我不确定这是否是您正在寻找的,但一种方法可能是一个抽象基类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; }
}

如果您的加载项/后端项不提供接口,那么应用程序的该部分将忽略它。 你可以用泛型来收紧这个问题,但我不确定这是否真的有必要。

使用集合和委托,您甚至可以使其更加抽象,从而提高运行时灵活性。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM