扩展方法导致LINQ出错

4

我认为我遇到了扩展方法和LINQ的一个特殊情况。

今天,我正在声明一些扩展方法,以使我的代码更易读。因此,我创建了一个扩展方法,它获取一个对象并执行直接转换:

public static class GeneralExtensions
{
    public static T Cast<T>(this object o)
    {
        return (T)o;
    }
}

意图是能够通过类似这样的方式调用我的直接强制转换:
MyObject.CastTo<MyInterface>();

恰好在同一命名空间中,我有一个具有LINQ表达式的扩展方法。

using System;
using System.Collections.Generic;
using System.Linq;

public static class EnumExtenstions
{
    public static IEnumerable<string> UseLinq(this IEnumerable<object> collection)
    {
        return (from object value in collection select value.ToString() ).ToList();
    }
}

将这个第一个扩展方法添加到我的代码库中会导致下一个错误

Error   CS1936  Could not find an implementation of the query pattern for source type 'object'.  'Select' not found.    

当两个扩展方法位于不同的命名空间(且未被引用),或将Cast重命名为其他名称,则可解决此问题。

我想更深入地了解这是为什么。是否与LINQ有重叠?如果是,为什么我的Cast具有优先权?

请在.NET Fiddle中查找代码(链接


你的 Cast 方法正在隐藏 Linq 的 Cast 方法。尝试重命名它。 - juharr
直接删除这个方法,它是不必要且危险的。实际上,即使没有任何“帮助”,使用它所需的字符数也比C#语言更多。 - Chris Marisic
因为它不明显是一个“as”转换(安全)还是直接转换(危险)。一般来说,您不应该使用直接转换,它们非常清楚地表明了糟糕的软件设计。直接转换几乎纯粹用于违反LSP(里氏替换原则)。https://en.wikipedia.org/wiki/Liskov_substitution_principle - Chris Marisic
@ChrisMarisic:我同意你的观点,但对你的推理并不完全赞同。假设我们限制讨论在引用类型上保持表示的转换。无论是"as"还是"direct"转换,都是开发人员告诉编译器:“我知道你不知道的一些事情,所以请相信我。”每当我不得不告诉编译器我对类型系统的了解超过它时,我就担心自己可能是错的!任何一种转换都表示程序非常复杂,以至于编译器无法对其进行可证明的类型安全性验证。 - Eric Lippert
@EricLippert 我同意,尽管有时候你想要放弃类型安全(确实非常“高级”/复杂)。对于从 SQL Reader 中提取数据之类的事情,“as”的价值也很大。https://dev59.com/DnE95IYBdhLWcg3wJKp_#2433275 我可以随时为语句添加更多限定词。离题了,OP 明显不是在询问复杂的动态编程和宽松类型,这就是我最初的陈述。 - Chris Marisic
显示剩余3条评论
1个回答

12

我希望能更深入地了解这是为什么。这与LINQ有什么重叠吗?

是的!

当你有一个形式为

from Foo bar in blah select baz

编译器会将其改写成一系列方法调用:

blah.Cast<Foo>().Select(bar => baz)
方法调用的解析方式与普通方法调用相同。如果blah有一个成员Cast,则使用它;否则,将搜索扩展方法。
“最接近的包含类获胜”是解析扩展方法的基本规则。如果您的扩展方法位于全局命名空间中的类中,而LINQ扩展方法位于System.Linq命名空间中的类中,则您的扩展方法更接近。为了进入您的类,编译器必须从当前命名空间向上到全局命名空间。为了进入System.Linq,编译器必须向上到全局命名空间,然后进入System.Linq以查找正确的类。
特别要注意的是,C#编译器不会“回溯”。它不会说“好吧,返回对象的Cast版本在我尝试使用Select时给了我一个错误;还有其他可以工作的Cast版本吗?”C#编译器只是说最好的Cast版本会出错,因此不应尝试找到一个更差的可以工作的Cast版本。在这种情况下,这是您想要的行为,但在许多情况下,您将得到意外调用的方法。C#更喜欢给出错误而不是尝试猜测您真正想要的方法。
这是实际规则的过度简化,在存在多个嵌套命名空间的多个“using”指令时,规则可能会变得复杂。如果需要确切的规则,请参考规范。
但最好不要首先进入那里。除非您打算复制LINQ的功能,否则不要制作自己的扩展方法命名为Cast、Select、Where等。
更新:我刚意识到这里潜在的更大问题:无论查询模式的方法与其名称冲突与否,您的转换方法可能无法按照您想要的方式执行。
我注意到,您的cast方法在许多方面都不如使用转换运算符:
- Cast<int>(123) 不必要地对int进行装箱;(int)123则不需要。 - Cast<short>(123) 失败,但(short)123成功。从一个装箱的int到short没有转换。 - 假设您定义了从Animal到Shape的用户定义转换。Cast<Shape>(new Tiger())失败,但(Shape)new Tiger()成功。 - 假设q是一个可空的int,而且恰好为空。 Cast<string>(q)成功!但(string)q在编译时将发生错误。
因此,您的cast方法与真正的转换运算符有一些重叠,但它绝不是它的替代品。如果您的意图是捕获转换运算符的语义,则需要使用dynamic,这会在运行时重新启动编译器,并对运行时类型进行编译时分析。

错误信息怎么样?一点也不清楚,也没有提供任何有用的提示来指示问题的来源! - disklosr
@disklosr:错误消息是写给使用查询语法创建查询的开发人员的,实现查询模式的方法必须在范围内。这里的具体情况是,查询模式的一个方法在范围内并且遮盖了正确的方法,坦白地说,在设计错误消息时我没有考虑到这种情况。这是自从我们设计该错误消息以来十年来我第一次想到它。 - Eric Lippert

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