【问题标题】:Defining/raising a generic event using constraints使用约束定义/引发通用事件
【发布时间】:2021-02-25 19:19:59
【问题描述】:

我想引发一个事件,允许返回 Widget(或任何派生类)类型的对象,具体类型通过泛型定义。

public class WidgetProcessor
{
   public event EventHandler<WidgetRequiredEventArgs<Widget>> WidgetRequired;

   public void DoSomethingThatNeedsAWidget<T>() where T: Widget
   {
      Widget widget = OnWidgetRequired<T>();

      //...now do something with the widget
   }

   private T OnWidgetRequired<T>() where T: Widget
   {
      T widget = null;

      if (this.WidgetRequired != null)
      {
         WidgetRequiredEventArgs<T> e = new WidgetRequiredEventArgs<T>();
         this.WidgetRequired(this, e);

         widget = e.Widget;
      }

      return widget;
   }
}

public class WidgetRequiredEventArgs<T>
   : EventArgs where T : Widget
{
   public WidgetRequiredEventArgs()
   {
   }

   public T Widget { get; set; }
}

OnWidgetRequired&lt;T&gt;()DoSomethingThatNeedsAWidget&lt;T&gt;() 上的约束允许我将指定类型限制为 Widget 或派生类。理想情况下,我会为事件声明做同样的事情,但它不支持使用 &lt;T&gt; 和对事件参数的约束,所以我必须将其明确声明为 Widget

但是,这会产生编译时错误:

CS1503:参数 2:无法从 'WidgetRequiredEventArgs' 转换为 'WidgetRequiredEventArgs'

对于e 参数就行了:

this.WidgetRequired(this, e);

那么为什么OnWidgetRequired&lt;T&gt;()WidgetRequiredEventArgs&lt;T&gt; 上的约束不满足事件处理程序的类型定义,我怎样才能让它编译呢?

【问题讨论】:

  • 您尝试做的事情是非法的,因为它不安全。您提出的语法将允许任何可以生成任何类型的Widget 的订阅者处理任何类型的WidgetWidget 的生成。 IE。即使OnWidgetRequired&lt;T&gt;() 的调用者被调用WidgetB,订阅者也可以产生WidgetA。如果您可以调整问题以描述不非法的操作,那么也许可以提供一个很好的答案。否则,这只是我们已经提出的所有其他“我想要的变体类型场景是非法的”问题的重复。
  • WidgetRequired 的实际用途是什么?如果此事件有多个订阅者会发生什么?
  • @PeterDuniho 看起来你是对的,这只是我们已经拥有的所有其他“我想要的变体类型场景是非法的”问题的重复,但其他人都没有足够清楚地解释为什么我的情况是非法的,因此我的问题。
  • @GuruStron 代码位于 UI 库中,事件实际上只是允许它在不知道/不依赖于它来自何处的情况下请求一个对象。所以在实践中,只有一个订阅者代表正在使用这个库的 UI 环境。

标签: c# generics


【解决方案1】:

C# 中的类不允许变化 (and for good reasons)。如果适合您的用例,您可以尝试为您的事件参数引入covariant 接口:

public interface  IWidgetRequiredEventArgs<out T> where T : Widget
{
   public T Widget { get; }
}

public class WidgetRequiredEventArgs<T>
   : EventArgs, IWidgetRequiredEventArgs<T> where T : Widget
{
   public WidgetRequiredEventArgs()
   {
   }

   public T Widget { get; set; }
}

并将其用于事件处理程序:

public event EventHandler<IWidgetRequiredEventArgs<Widget>> WidgetRequired;

或者您可以将WidgetProcessor 设为通用:

public class WidgetProcessor<T> where T: Widget
{
   public event EventHandler<WidgetRequiredEventArgs<T>> WidgetRequired;

   public void DoSomethingThatNeedsAWidget() 
   {
      Widget widget = OnWidgetRequired();

      //...now do something with the widget
   }

   private T OnWidgetRequired()
   {
       ....
   }
}

【讨论】:

  • 这不起作用,因为正如您在原始代码中看到的,该属性由事件处理程序设置,并由引发事件的代码检索。类型参数不能同时是协变和逆变的,但这是实现原始问题中的逻辑所必需的。您回答了一个不同的问题。虽然现在我想起来了……也许如果你改变接口,让它是逆变的而不是协变的。
  • @PeterDuniho 事件处理程序(即 OnWidgetRequired)仍会创建一个具体类型,因此它可以设置它,无需更改。这几乎可以编译。
  • 它需要在与原始问题相同的上下文中编译。 IE。不仅显示上面的声明,还显示事件、订阅者和引发事件的代码,所有这些都协同工作。请密切注意在 OP 的代码中,处理程序正在 设置 事件 args 属性。
  • @PeterDuniho 代码中没有实际的处理程序,但是是的,看起来这就是目标。
  • “也许如果你改变接口,让它是逆变的而不是协变的。” -- 哦,我错了......这只是将问题转移到处理程序的调用。仍然不起作用,并且有充分的理由
【解决方案2】:

您的示例中有一个基本问题。您希望事件的订阅者能够生成您在泛型方法中指定的对象类型。 IE。您的方法创建 args 对象,处理程序设置属性,方法引发属性,然后方法获取属性值。

但是,如果您可以按照您的要求进行操作,那么生产者可以设置他们旨在处理的任何类型的Widget,即使它不是调用该方法来检索的Widget 的类型。

或者换句话说,属性的泛型类型不能是变体,因为只有协变或逆变是合法的,不能同时是两者。

一种方法是将事件本身移动到泛型类型中,而不是使用泛型方法。这允许在每个步骤中使用正确的类型声明事件。框架将为代码中使用的每个泛型类型参数实例化一个带有单独事件的新具体类型。

例如,这是一个简单的通用实现(我已将类名从 Widget 重命名为更易读的名称,因此类型之间的关系更清晰,为了简单起见,我将所有内容都设为 static.. .当然,如果您有这样一种场景,即使用相同的泛型类型参数有多个事件是有意义的,您可以将这些设为非静态):

class Base { }
class Derived1 : Base { }
class Derived2 : Base { }

class BaseEventArgs<T> : EventArgs where T : Base
{
    public T Item { get; set; }
}

static class ItemHandler<T> where T : Base
{
    public static event EventHandler<BaseEventArgs<T>> ItemProducer;

    public static T OnBaseRequired()
    {
        BaseEventArgs<T> args = new BaseEventArgs<T>();

        ItemProducer?.Invoke(null, args);

        return args.Item;
    }
}

然后你就可以像这样使用它:

public void DoSomethingThatNeedsABase<T>() where T : Base
{
    Base widget = ItemHandler<T>.OnBaseRequired();

    //...now do something with the "Base", aka "widget
}

【讨论】:

  • 所以为了真正简化答案,T 可能被指定为 Derived1,但因为事件处理程序接受任何类型的 Base,所以有人可以传入 Derived2。啊,一旦你这样描述它就很明显了。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2019-10-24
  • 1970-01-01
相关资源
最近更新 更多