【问题标题】:C# "Semi-Generic" Interface?C#“半通用”接口?
【发布时间】:2021-04-23 08:48:54
【问题描述】:

概述

简单地说,我希望创建一个由两个类实现的接口。然而,由于它们的用例,接口和其中的方法很难声明。

本质上,这两个类实现了这个新接口。每种情况下的接口方法Foo()都会以某种方式修改引用的对象并输出原始对象的副本。

public class MyClass1
{
    private int _value;

    public MyClass1(int val) { _value = val; }

    public void Foo(out MyClass1 result)
    {
        result = new MyClass1(_value);
        _value += 1;
    }
}

public class MyClass2
{
    private double _value;

    public MyClass2(double val) { _value = val; }

    public void Foo(out MyClass2 result)
    {
        result = new MyClass2(_value);
        _value += 1.5;
    }
}

我希望找到最直观、最优雅的方式来声明和实现interface IInterface 以及void Foo()

尝试的解决方案

以下是我尝试过的一些选项以及每个选项的明显优缺点。

选项 1:创建通用接口

public interface IInterface<T> where T : class
{
    void Foo(out T result);
}

优点

  • 允许在每个类中实现泛型
  • 其他地方易于使用的方法
  • 类型安全的方法实现和使用

缺点

  • 令人困惑的类定义

这个解决方案唯一明显的问题是,在我们的例子中,“通用”类型T 始终是实现接口的类,而不是任意类型(即IEnumerable&lt;T&gt;)。虽然这个解决方案效果很好,但它确实会在类实现中造成难看的冗余:

public class MyClass1 : IInterface<MyClass1> // Ew!
{
    // ...

    public void Foo(out MyClass1 result) { /* ... */ }
}

尽管类定义很奇怪,但方法的使用非常简单:

void Bar(MyClass1 orig)
{
    orig.Foo(out MyClass1 copy);
    
    // ...
}

选项 2:使接口方法本身通用

public interface IInterface
{
    void Foo<T>(out T result) where T : IInterface; // Gross!
}

优点

  • 简单的类定义

缺点

  • 令人困惑的方法声明
  • 需要在方法实现中添加类型安全逻辑
  • 易于在其他地方误用的方法

这个选项有更大的问题。方法声明比较混乱,实现中需要额外的错误处理:

public class MyClass1 : IInterface
{
    // ...

    public void Foo<T>(out T result) where T : IInterface // Gross again!
    {
        // Nothing preventing mismatched types here...
        if (typeof(T) != typeof(MyClass1))
            throw new Exception("Uh oh!");

        // ...
    }
}

如前所述,由于类型限制较宽松,该方法也很容易误用:

void Bar(MyClass1 orig)
{
    // Type mismatch!
    orig.Foo<MyClass2>(out MyClass2 copy);
    
    // ...
}

选项3:使用接口作为输出参数

public interface IInterface
{
    void Foo(out IInterface result);
}

优点

  • 简单的类定义
  • 简单的方法声明

缺点

  • 方法的实现和使用中的类型不匹配风险

虽然此解决方案消除了所有冗余泛型,但遗憾的是,它在方法的实现和使用中为类型不匹配错误留下了空间。

public class MyClass1 : IInterface
{
    // ...

    public void Foo(out IInterface result)
    {
        // ...

        // Could output MyClass2 instead!
        result = new MyClass1(_value);
    }
}

上面的问题是相当微不足道的,但下面的情况绝对是潜在的问题:

void Bar(MyClass1 orig)
{
    orig.Foo(out IInterface copy);

    // Can cast copy as wrong type
    MyClass2 a = (MyClass2)copy;

    // Same problem can happen with 'as' keyword
    MyClass2 b = copy as MyClass2;
    
    // ...
}

最终目标

虽然我认为目前不可能,但如果能够使用“伪通用”out T 定义接口方法Foo() 会更简洁,仅限于实现类仅。像这样的东西是理想的:

public interface IInterface
{
    // In this context, I would want "this" or some other keyword
    // (self, etc.) to indiciate the class implementing the interface

    void Foo(out T result) where T : this;
}

public class MyClass1 : IInterface
{
    // In an ideal world, this would be valid and correct
    void Foo(out MyClass1 result) { /* ... */ }
}

此解决方案将:

  • 消除丑陋、冗余的类定义
  • 保持方法实现简单且类型安全
  • 允许在其他情况下使用简单、安全的方法(见下文)
void Bar(MyClass1 orig)
{
    // Allow concrete type here rather than interface
    orig.Foo(out MyClass1 copy);

    // ...
}

总结

感谢阅读!很想听听任何人对用于此类案例的最佳方法或我尝试过的解决方案的任何潜在替代方案的想法。

【问题讨论】:

  • 目前还不清楚引入接口本身是为了解决什么问题。似乎还有其他问题 - 即这个接口的非专业用户是什么样的,他们如何声明将作为out引用传递的变量?
  • Microsoft 自己在整个 BCL 中广泛使用选项 1。如果您唯一的抱怨是它“看起来很丑”,那真的不是打折它的正当理由,特别是因为它比您提供的任何其他选项都更好地 WRT 类型安全性。
  • 2 是错误的,因为你不想要giraffe.Foo&lt;Elephant&gt;()。 3 感觉不好,你需要总是投射结果。

标签: c# oop generics


【解决方案1】:

没有通用的where T : this 约束。如果您认为这是一个需要解决的问题,您可以向https://github.com/dotnet/csharplang提交请求

最接近的替代方案可能是“奇怪地重复出现的模板模式”:interface IInterface&lt;T&gt; where T : IInterface&lt;T&gt;,但在这种情况下,与您的第一个替代方案相比,它似乎没有添加任何内容。

我不同意第一个替代方案有一个令人困惑的类声明。虽然类型是重复的,但这是一个相当小的问题,似乎只在相当特定的情况下才会发生。

【讨论】:

  • 我同意 1 中的类定义并不像 OP 所说的那么严重。虽然简单的继承/实现从来没有这种递归模式,但通用继承/实现确实允许它,并且它提供了 OP 正在寻找的特定用例。另外,它是唯一一个不允许返回错误类型的方法。
【解决方案2】:

您可以不走接口路线,而是改用子类化吗?使每个类的顶级都派生自父级。更简单的声明。然后你的其他类可以包含任何其他关于它们自己的不同之处。类似的东西

public class BaseClass<T,T2> where T2 : new()
{
    public virtual T2 Foo()
    {
        var newObj = new T2();
        // anything else you want to do with it?
        return newObj;
    }

    protected T _value;
}

public class BaseOne : BaseClass<int, BaseOne>
{
    public BaseOne()
    {
        _value = 1;
    }
}

public class BaseTwo : BaseClass<double, BaseTwo>
{
    public BaseTwo()
    {
        _value = 1.5;
    }
}

在声明中,第一个“T”是您的数据类型 int 或 double。第二个“T2”要求允许通过“new()”调用创建。因此,通过将您正在构建的类提供为 T2,您可以直接创建该类类型。

由于每个单独的子类都有其没有参数的默认构造函数,并且“_value”属性为受保护的(可在父类的子类级别更改),因此它们可以根据需要分别将值初始化为1或1.5 (或您实际需要的任何值)。

因此每种类型的构造函数分别用 1 或 1.5 处理启动“_value”。单个 FOO 调用创建并返回没有问题的新实例。然后,当尝试分别创建每个对象时,它看起来很简单

        var x1 = new BaseOne();
        var y1 = new BaseTwo();

因为 each 的类声明已经声明为:

BaseClass<T,??> of <int, BaseOne> or <double, BaseTwo> 

相应地,您完全屏蔽了 new() 调用中的类型。

然后,获取相同类型的新实例

        var x2 = x1.Foo();
        var y2 = y1.Foo();

此外,如果有许多其他属性/值您正试图继承,您可能会寻找 CLONE(),而不是执行 NEW() 调用。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2019-10-16
    • 1970-01-01
    • 1970-01-01
    • 2022-01-25
    • 2014-02-07
    • 2019-10-06
    • 2011-10-12
    相关资源
    最近更新 更多