没有类型推断的通用扩展方法

12
我有如下方法:
public static TEventInvocatorParameters Until
    <TEventInvocatorParameters, TEventArgs>(this TEventInvocatorParameters p,
                                            Func<TEventArgs, bool> breakCond)
    where TEventInvocatorParameters : EventInvocatorParameters<TEventArgs>
    where TEventArgs : EventArgs
{
    p.BreakCondition = breakCond;
    return p;
}

而这个类

public class EventInvocatorParameters<T>
    where T : EventArgs
{
    public Func<T, bool> BreakCondition { get; set; }
    // Other properties used below omitted for brevity.
}

现在,我有以下问题:
  1. 这个扩展方法会显示在所有类型上,甚至是 string
  2. 我无法编写 new EventInvocatorParameters<EventArgs>(EventABC).Until(e => false); 它告诉我“从用法中无法推断出方法的类型参数。”
我不能像这样使用泛型类型参数吗?你如何解决这个问题? 重要提示:我需要这两个泛型参数,因为我需要返回调用此扩展方法时所调用的相同类型。
更广泛的背景(回答问题不必要!): 我正在尝试创建一个流畅的接口来调用事件。 基础是这个静态类:
public static class Fire
{
   public static void Event<TEventArgs>(
       ConfiguredEventInvocatorParameters<TEventArgs> parameters)
    where TEventArgs : EventArgs
    {
        if (parameters.EventHandler == null)
        {
            return;
        }

        var sender = parameters.Sender;
        var eventArgs = parameters.EventArgs;
        var breakCondition = parameters.BreakCondition;

        foreach (EventHandler<TEventArgs> @delegate in 
                 parameters.EventHandler.GetInvocationList())
        {
            try
            {
                @delegate(sender, eventArgs);
                if (breakCondition(eventArgs))
                {
                    break;
                }
            }
            catch (Exception e)
            {
                var exceptionHandler = parameters.ExceptionHandler;
                if (!exceptionHandler(e))
                {
                    throw;
                }
            }
        }
    }
}

为了确保此方法只能使用完全配置的参数进行调用,它只接受从 EventInvocatorParameters<T> 派生的 ConfiguredEventInvocatorParameters<T>
public class ConfiguredEventInvocatorParameters<T>
    : EventInvocatorParameters<T>
    where T : EventArgs
{
    public ConfiguredEventInvocatorParameters(
        EventInvocatorParameters<T> parameters, object sender, T eventArgs)
        : base(parameters)
    {
        EventArgs = eventArgs;
        Sender = sender;
    }

    public T EventArgs { get; private set; }
    public object Sender { get; private set; }

}

以下调用将是有效的:
Fire.Event(EventName.With(sender, eventArgs));
Fire.Event(EventName.With(sender, eventArgs).Until(e => e.Cancel));
Fire.Event(EventName.Until(e => e.Cancel).With(sender, eventArgs));

以下内容是无效的:
// no sender or eventArgs have been specified, i.e. missing call to With(...)
Fire.Event(EventName.Until(e => e.Cancel));

为了使其工作,存在名为With的扩展方法,它们接受EventHandler<TEventArgsTEventInvocatorParameters,并返回ConfiguredEventInvocatorParameters<TEventArgs>。现在,在With之后的所有调用也需要返回类型ConfiguredEventInvocatorParameters<TEventArgs>,否则有效调用的第二个示例(以Until结尾)将不起作用。
如果您对API有任何想法,请让我知道。然而,我想避免以下三件事情:
  • 仅在运行时失败,如果参数没有完全配置
  • 创建倒置语法,例如EventName.With(...).Until(...).Fire()
  • 使用臭名昭著的Do方法来启动事物:Fire(EventName).With(...).Until(...).Do();
4个回答

18
2020年11月更新:以下原始答案写于2011年;近期C#的版本中,泛型方法类型推断、重载解析以及方法的“最终验证”规则都有小但显著的改变;本答案和我原来在MSDN博客上关于此问题的文章链接可能已经不准确。此外,出于法律原因,Microsoft删除了原始文章的评论;那些评论中有大量背景和讨论。我希望有一天能够抽出时间重新审视这篇文章,澄清现在的规则、它们如何改变以及那些被删除的评论对这些决策产生了什么影响,但那是很多工作,我可能暂时做不到。请记住,自2012年11月以来,我已经不在C#语言设计团队中。


泛型方法类型推断故意不从限制中进行任何推断。相反,推断是从参数和形式参数中进行的,然后将推导出的类型参数与限制进行检查。

有关约束和方法签名的某些设计问题的详细讨论,包括数十人告诉我认为现有设计合理是错误的,请参阅我的文章:

https://learn.microsoft.com/en-gb/archive/blogs/ericlippert/constraints-are-not-part-of-the-signature


3
我喜欢你回答中的这一部分:“包括几十个人告诉我,我认为现有设计是明智的是错误的”! :-) 你能提供一个实现我想要的功能的替代方案吗? - Daniel Hilgarth
顺便说一句:我同意他们的观点。不想重新开始那个讨论:你所说的最佳匹配在技术上可能是正确的,但直觉上并不是。所有告诉你错误的答案应该足以证明这一点。我认为你的主要观点是无效的。没有猜测。我完全同意David Nelson在2009年12月10日下午4:13的回答。你对他的回答并没有更有说服力。主要问题是编译器推断出了一个对于该泛型方法无效的类型。 - Daniel Hilgarth
@Daniel:问题在于不同的情况会让你的直觉朝不同的方向发展。我坚持我的立场:C#是一种语言,它会告诉你当某些东西看起来有问题时。它不会忽略问题并退回到更糟糕的选择,当推断出的最佳选择违反约束条件时。 - Eric Lippert
2
我知道你坚持你的立场。所有那些争论都不能让你改变,那么为什么我的会呢?然而,如果你提供一个实际展示你认为是问题的样例,你可能会提高理解。想象一下如果C#是以另一种方式实现的,哪些情况将无法直观地理解?它会引起哪些错误和问题?因为我 - 显然还有很多其他人 - 不明白这会如何“忽略问题”。 - Daniel Hilgarth
在我看来,这是一个设计缺陷。如果你为一个泛型类型定义了两个扩展方法重载,并将第二个重载约束为Foo类型,则编译器在从类型为Spam的源对象调用时无法聪明地选择第一个重载,但它足够聪明地告诉你源对象与类型约束不匹配。扩展方法的整个目的是为了方便,但当你不能使用它们时,因为编译器无法聪明地选择正确的重载,而又能聪明地告诉你它选择了错误的重载时,就没有任何方便可言了。 - Stephen Bunch
显示剩余4条评论

3

对于任何对此感兴趣的人,目前,我通过使用一个通用类层次结构来解决了原始问题(流畅的事件调用API)。这基本上就是Hightechrider答案的升级版。

public abstract class EventInvocatorParametersBase
    <TEventInvocatorParameters, TEventArgs>
    where TEventArgs : EventArgs
    where TEventInvocatorParameters :
        EventInvocatorParametersBase<TEventInvocatorParameters, TEventArgs>

{
    protected EventInvocatorParametersBase(
        EventHandler<TEventArgs> eventHandler,
        Func<Exception, bool> exceptionHandler,
        Func<TEventArgs, bool> breakCondition)
    {
        EventHandler = eventHandler;
        ExceptionHandler = exceptionHandler;
        BreakCondition = breakCondition;
    }

    protected EventInvocatorParametersBase(
        EventHandler<TEventArgs> eventHandler)
        : this(eventHandler, e => false, e => false)
    {
    }

    public Func<TEventArgs, bool> BreakCondition { get; set; }
    public EventHandler<TEventArgs> EventHandler { get; set; }
    public Func<Exception, bool> ExceptionHandler { get; set; }

    public TEventInvocatorParameters Until(
        Func<TEventArgs, bool> breakCondition)
    {
        BreakCondition = breakCondition;
        return (TEventInvocatorParameters)this;
    }

    public TEventInvocatorParameters WithExceptionHandler(
        Func<Exception, bool> exceptionHandler)
    {
        ExceptionHandler = exceptionHandler;
        return (TEventInvocatorParameters)this;
    }

    public ConfiguredEventInvocatorParameters<TEventArgs> With(
        object sender, 
        TEventArgs eventArgs)
    {
        return new ConfiguredEventInvocatorParameters<TEventArgs>(
            EventHandler, ExceptionHandler, BreakCondition,
            sender, eventArgs);
    }
}

public class EventInvocatorParameters<T> :
    EventInvocatorParametersBase<EventInvocatorParameters<T>, T>
    where T : EventArgs
{
    public EventInvocatorParameters(EventHandler<T> eventHandler)
        : base(eventHandler)
    {
    }
}

public class ConfiguredEventInvocatorParameters<T> :
    EventInvocatorParametersBase<ConfiguredEventInvocatorParameters<T>, T>
    where T : EventArgs
{
    public ConfiguredEventInvocatorParameters(
        EventHandler<T> eventHandler,
        Func<Exception, bool> exceptionHandler,
        Func<T, bool> breakCondition, object sender,
        T eventArgs)
        : base(eventHandler, exceptionHandler, breakCondition)
    {
        EventArgs = eventArgs;
        Sender = sender;
    }

    public ConfiguredEventInvocatorParameters(EventHandler<T> eventHandler,
                                              object sender,
                                              T eventArgs)
        : this(eventHandler, e => false, e => false, sender, eventArgs)
    {
    }

    public T EventArgs { get; private set; }
    public object Sender { get; private set; }
}

public static class EventExtensions
{
    public static EventInvocatorParameters<TEventArgs> Until<TEventArgs>(
        this EventHandler<TEventArgs> eventHandler,
        Func<TEventArgs, bool> breakCondition)
        where TEventArgs : EventArgs
    {
        return new EventInvocatorParameters<TEventArgs>(eventHandler).
            Until(breakCondition);
    }

    public static EventInvocatorParameters<TEventArgs> 
        WithExceptionHandler<TEventArgs>(
            this EventHandler<TEventArgs> eventHandler,
            Func<Exception, bool> exceptionHandler)
        where TEventArgs : EventArgs
    {
        return
            new EventInvocatorParameters<TEventArgs>(eventHandler).
                WithExceptionHandler(exceptionHandler);
    }

    public static ConfiguredEventInvocatorParameters<TEventArgs>
        With<TEventArgs>(
            this EventHandler<TEventArgs> eventHandler, object sender,
            TEventArgs eventArgs)
        where TEventArgs : EventArgs
    {
        return new ConfiguredEventInvocatorParameters<TEventArgs>(
            eventHandler, sender, eventArgs);
    }
}

这使您可以编写如下代码:
```html

这使您可以编写如下代码:

```
Fire.Event(EventName.WithExceptionHandler(e => false)
                    .Until(e => false).With(this, EventArgs.Empty));
Fire.Event(EventName.With(this, EventArgs.Empty));
Fire.Event(EventName.WithExceptionHandler(e => false)
                    .With(this, EventArgs.Empty).Until(e => false));
Fire.Event(EventName.With(this, EventArgs.Empty)
                    .WithExceptionHandler(e => false).Until(e => false));

但是它不允许您编写此内容,因为并未提供所有必要的信息(eventArgs和sender):
Fire.Event(EventName.Until(e => false));
Fire.Event(EventName);

1

你有必要使用扩展方法吗?如果你将Until方法放在EventInvocatorParameters<T>类中,你可以避免上述两个问题:

public class EventInvocatorParameters<T>
    where T : EventArgs
{
    public Func<T, bool> BreakCondition { get; set; }
    // Other properties used below omitted for brevity.

    public EventInvocatorParameters<T> Until (Func<T, bool> breakCond)
    {
        this.BreakCondition = breakCond;
        return this;
    }
}

是的,有一个理由。它详细说明在题目的长篇章节中,标题为“更广泛的视角(回答问题并非必要!)”。也许毕竟还是需要的。简短版本:Until可能会在EventInvocatorParameters<T>的派生类上调用,如果是这种情况,Until的返回类型需要是那个派生类型。 - Daniel Hilgarth

0

我知道这有点投降,但您是否考虑过使用Rx而不是重新发明您似乎正在尝试做的事情?


没有,我在谷歌上搜索“流畅的事件调用API”没有找到相关信息。感谢您的指引,我会进一步了解它。 - Daniel Hilgarth
1
请问您能否给我一个简短的例子,说明如何使用 Rx 来实现我想要的功能? - Daniel Hilgarth

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接