【问题标题】:Null propagation operator and extension methods空传播运算符和扩展方法
【发布时间】:2014-07-31 15:33:24
【问题描述】:

我一直在研究 Visual Studio 14 CTP 以及 C# 6.0 并使用空传播运算符。

但是,我找不到以下代码无法编译的原因。这些功能尚未记录,因此我不确定这是一个错误还是扩展方法根本不受 ?. 运算符的支持,并且错误消息具有误导性。

class C
{
    public object Get()
    {
        return null;
    }
}

class CC
{
}

static class CCExtensions
{
    public static object Get(this CC c)
    {
        return null;
    }
}

class Program
{
    static void Main(string[] args)
    {
        C c = null;
        var cr = c?.Get();   //this compiles (Get is instance method)

        CC cc = null;
        var ccr = cc?.Get(); //this doesn't compile

        Console.ReadLine();
    }
}

错误信息是:

“ConsoleApplication1.CC”不包含“Get”的定义,并且找不到接受“ConsoleApplication1.CC”类型的第一个参数的扩展方法“Get”(您是否缺少 using 指令或程序集引用? )

【问题讨论】:

  • 如果去掉空传播操作符,代码能编译吗?如果不是,则可能是编译器找到扩展方法而不是运算符的问题。

标签: c# roslyn c#-6.0


【解决方案1】:

空传播运算符的要点是避免取消引用 null 值。

但是,扩展方法不尊重它们的 this 参数,实际上可以在 null 值上完美调用。
(虽然方法本身如果不期望null的话很可能会抛出异常)

因此,尚不清楚 null 安全扩展方法调用是否会跳过调用。

【讨论】:

  • 看来,基于错误消息(假设行为没有改变)?. 运算符仅在语法上对实例方法有效,对扩展方法无效。这实际上似乎是解决问题的明智之举。
  • @Servy:是的;这可能就是他们不允许这样做的原因。
  • @ChrisDunaway:错了。扩展方法的全部意义在于使用实例调用它。
  • @Slaks - 呃!我在想调用一个常规的静态方法。大脑冻结!
  • 这是错误的。设计扩展方法来处理 this-parameter 为 null - 是一种代码味道!大多数 BCL 扩展方法(以及我知道的任何其他方法)都像以前那样抛出 ArgumentNullException。扩展方法应该“看起来像”并且表现得像实例方法。
【解决方案2】:

我不在 Roslyn 团队工作,但我相当确信这是一个错误。我看了一下源代码,我可以解释发生了什么。

首先,我不同意 SLaks 的回答,即不支持此功能,因为扩展方法不会取消引用它们的 this 参数。这是一个毫无根据的说法,考虑到thedesigndiscussions 中的任何一个都没有提到它。另外,运算符的语义变成了大致看起来像三元运算符 ((obj == null) ? null : obj.Member) 的东西,因此从技术意义上说它不能得到支持并不是一个很好的理由。我的意思是,归结为生成的代码,实例方法上的隐式this 和静态扩展方法上的显式this 确实没有区别。

错误消息是一个很好的线索,表明这是一个错误,因为它抱怨该方法不存在,而实际上它确实存在。您可能已经通过从调用中删除条件运算符,改用成员访问运算符并成功编译代码来测试这一点。如果这是对运算符的非法使用,您将收到类似于以下内容的消息:error CS0023: Operator '.' cannot be applied to operand of type '<type>'

错误在于,当Binder 尝试将语法绑定到已编译的符号时,它使用了 private static NameSyntax GetNameSyntax(CSharpSyntaxNode, out string) [link] 方法,该方法无法返回尝试绑定时所需的方法名称调用表达式(我们的方法调用)。

一种可能的解决方法是在GetNameSyntax[link] 的开关中添加额外的case 语句,如下所示(文件:Compilers/CSharp/Source/Binder/Binder_Expressions.cs:2748):

// ...
case SyntaxKind.MemberBindingExpression:
     return ((MemberBindingExpressionSyntax)syntax).Name;
// ...

这可能被忽略了,因为将扩展方法作为成员调用的语法,即使用成员访问运算符)最终使用了一组与成员访问运算符与条件访问运算符不同的语法,具体来说, ?. 运算符使用了 MemberBindingExpressionSyntax,而 GetNameSyntax 方法没有考虑到该 MemberBindingExpressionSyntax

有趣的是,第一个编译的var cr = c?.Get(); 没有填充方法名称。但是,它可以工作,因为首先为该类型找到本地方法组成员,然后将其传递给BindInvocationExpression [link] 的调用。当方法is being resolved(注意在尝试BindExtensionMethod [link]之前调用ResolveDefaultMethodGroup [link]),它首先检查这些方法并找到它。在扩展方法的情况下,它会尝试找到与传递给方法的方法名称匹配的扩展方法,在这种情况下,它是一个空字符串而不是Get,并导致显示错误错误。

使用我的本地版本的 Roslyn 和我的错误修复,我得到一个编译的程序集,其代码看起来像(使用 dotPeek 重新生成):

internal class Program
{
    private static void Main(string[] args)
    {
        C c1 = (C) null;
        object obj1 = c1 != null ? c1.Get() : (object) null;
        CC c2 = (CC) null;
        object obj2 = c2 != null ? CCExtensions.Get(c2) : (object) null;
        Console.ReadLine();
    }
}

【讨论】:

    【解决方案3】:

    是的。这是一个错误。感谢您提出这个问题。 该示例应该编译并且应该导致 Get 的条件调用,无论 Get 是否是扩展。

    “?”的用法。在 cc?.Get() 中表明调用者希望 cc 在继续之前进行空值检查。即使 Get 可以以某种方式处理 null,调用者也不希望这种情况发生。

    【讨论】:

    猜你喜欢
    • 2015-02-26
    • 2010-10-15
    • 1970-01-01
    • 2018-12-06
    • 2017-12-20
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多