扩展方法和编译时检查

发布于 2024-09-27 16:41:12 字数 1360 浏览 3 评论 0原文

也许有点棘手,但我想知道为什么。在 System.Core.dllSystem.Linq.Enumerable.cs 中,我们有:

public static int Count<TSource>(this IEnumerable<TSource> source);

在我的代码中,我正在做一些邪恶的事情:

namespace Test
{
   public static class Extensions
   {
     public static int Count<TSource>(this IEnumerable<TSource> source)
     {
        return -1; //evil code
     }
   }

   //commented temporarily
   //public static class CommentedExtensions
   //{
   //  public static int Count<TSource>(this IEnumerable<TSource> source)
   //  {
   //     return -2; //another evil code
   //  }
   //}

   public static void Main(string[] args)
   {
     Console.WriteLine(Enumerable.Range(0,10).Count());   // -1, evil code works
     Console.Read();
   }
}

如果我取消注释 CommentedExtensions,我会收到一个编译错误,如预期的那样显示“此调用不明确 blabla”。但为什么我第一次没有得到这个错误呢?也很暧昧啊!

编辑 经过另一次测试,我发现如果扩展方法位于不同的命名空间中,即使它们完全相同,也不会出现编译错误。为什么允许?它带来了 c# 中方法调用的不明确性。

EDIT2 我知道事实上这两个 Count 在 IL 中是不同的。事实上,它正在调用

Enumerable.Count(Enumerable.Range(0,10))

,而我的邪恶扩展方法正在调用:

MyExtension.Count(Enumerable.Range(0,10))

所以它们是不同的。但我还是觉得那味道很难闻。我们有“真正的”扩展方法吗?哪些可以阻止邪恶行为?

Maybe a little tricky, but I wonder why. In System.Linq.Enumerable.cs of System.Core.dll we have:

public static int Count<TSource>(this IEnumerable<TSource> source);

In my code I'm doing something evil:

namespace Test
{
   public static class Extensions
   {
     public static int Count<TSource>(this IEnumerable<TSource> source)
     {
        return -1; //evil code
     }
   }

   //commented temporarily
   //public static class CommentedExtensions
   //{
   //  public static int Count<TSource>(this IEnumerable<TSource> source)
   //  {
   //     return -2; //another evil code
   //  }
   //}

   public static void Main(string[] args)
   {
     Console.WriteLine(Enumerable.Range(0,10).Count());   // -1, evil code works
     Console.Read();
   }
}

If I uncomment CommentedExtensions, I'll get a compile error saying "this call is ambiguous blabla" as expected. But why I didn't get this error at the first time? It's also ambiguous!

EDIT After another test, I found that I won't get compile errors if the extension methods are in different namespaces, even they are completely the same. Why it's allowed? It brings ambiguous call of methods in c#.

EDIT2 I know in fact the two Count are different in IL. In fact it's calling

Enumerable.Count(Enumerable.Range(0,10))

and my evil extension method is calling:

MyExtension.Count(Enumerable.Range(0,10))

so they are different. But still I think it's a bad smell. Do we have "real" extension methods? which can prevent the evil behavior?

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(4

忘年祭陌 2024-10-04 16:41:12

C# 语言规范 的第 7.6.5.2 节介绍了编译器如何确定哪个扩展方法在范围内,以及哪些扩展方法优先于其他方法:

搜索 C [(候选扩展方法)] 的过程如下:

  • 从最接近的封闭命名空间声明开始,继续每个封闭命名空间声明,并以包含的编译单元结束,连续尝试查找扩展方法的候选集:
    • 如果给定的命名空间或编译单元直接包含具有合格扩展方法 Mj 的非泛型类型声明 Ci,则这些扩展方法的集合就是候选集
    • 如果在给定命名空间或编译单元中使用命名空间指令导入的命名空间直接包含具有合格扩展方法 Mj 的非泛型类型声明 Ci,则这些扩展方法的集合就是候选集合。

这意味着,如果您的扩展方法与调用它们的代码位于同一名称空间中,则会选择这些扩展方法。将选择封闭命名空间中的扩展方法,而不是已导入的其他命名空间。

Section 7.6.5.2 of the C# language specification describes how the compiler determines which extension methods are in scope, and which extension methods take precedence over others :

The search for C [(a candidate extension method)] proceeds as follows:

  • Starting with the closest enclosing namespace declaration, continuing with each enclosing namespace declaration, and ending with the containing compilation unit, successive attempts are made to find a candidate set of extension methods:
    • If the given namespace or compilation unit directly contains non-generic type declarations Ci with eligible extension methods Mj, then the set of those extension methods is the candidate set
    • If namespaces imported by using namespace directives in the given namespace or compilation unit directly contain non-generic type declarations Ci with eligible extension methods Mj, then the set of those extension methods is the candidate set.

Meaning that if you have extension methods in the same namespace than the code from which you call them, these extension methods are selected. Extension methods in an enclosing namespace will be choosed over other namespaces that have been imported.

若能看破又如何 2024-10-04 16:41:12

看来 C# 首先在当前命名空间中查找

在本例中,IL 是

.method public hidebysig static void  Main(string[] args) cil managed
{
  .entrypoint
  // Code size       27 (0x1b)
  .maxstack  8
  IL_0000:  nop
  IL_0001:  ldc.i4.0
  IL_0002:  ldc.i4.s   10
  IL_0004:  call       class [mscorlib]System.Collections.Generic.IEnumerable`1<int32> [System.Core]System.Linq.Enumerable::Range(int32,
                                                                                                                                  int32)
  IL_0009:  call       int32 Test.Extensions::Count<int32>(class [mscorlib]System.Collections.Generic.IEnumerable`1<!!0>)
  IL_000e:  call       void [mscorlib]System.Console::WriteLine(int32)
  IL_0013:  nop
  IL_0014:  call       int32 [mscorlib]System.Console::Read()
  IL_0019:  pop
  IL_001a:  ret
} // end of method Program::Main

如果我将 main 方法移至另一个命名空间 (XXX),在这种情况下,编译器会将该方法解析为 System.Linq 版本

namespace Test
{
    public static class Extensions
    {
        public static int Count<TSource>(this IEnumerable<TSource> source)
        {
            return -1; //evil code
        }
    }

}

namespace XXX{

    public static class Program
    {
        public static void Main(string[] args)
        {
            Console.WriteLine(Enumerable.Range(0, 10).Count());   // -1, evil code works
            Console.Read();
        }
   }
}


.method public hidebysig static void  Main(string[] args) cil managed
{
  .entrypoint
  // Code size       27 (0x1b)
  .maxstack  8
  IL_0000:  nop
  IL_0001:  ldc.i4.0
  IL_0002:  ldc.i4.s   10
  IL_0004:  call       class [mscorlib]System.Collections.Generic.IEnumerable`1<int32> [System.Core]System.Linq.Enumerable::Range(int32,
                                                                                                                                  int32)
  IL_0009:  call       int32 [System.Core]System.Linq.Enumerable::Count<int32>(class [mscorlib]System.Collections.Generic.IEnumerable`1<!!0>)
  IL_000e:  call       void [mscorlib]System.Console::WriteLine(int32)
  IL_0013:  nop
  IL_0014:  call       int32 [mscorlib]System.Console::Read()
  IL_0019:  pop
  IL_001a:  ret
} // end of method Program::Main

要在后一个示例中显式使用您的方法,请使用

namespace XXX{
    using Test;
    public static class Program
    {
        public static void Main(string[] args)
        {
            Console.WriteLine(Enumerable.Range(0, 10).Count());   // -1, evil code works
            Console.Read();
        }

    }
}

.method public hidebysig static void  Main(string[] args) cil managed
{
  .entrypoint
  // Code size       27 (0x1b)
  .maxstack  8
  IL_0000:  nop
  IL_0001:  ldc.i4.0
  IL_0002:  ldc.i4.s   10
  IL_0004:  call       class [mscorlib]System.Collections.Generic.IEnumerable`1<int32> [System.Core]System.Linq.Enumerable::Range(int32,
                                                                                                                                  int32)
  IL_0009:  call       int32 Test.Extensions::Count<int32>(class [mscorlib]System.Collections.Generic.IEnumerable`1<!!0>)
  IL_000e:  call       void [mscorlib]System.Console::WriteLine(int32)
  IL_0013:  nop
  IL_0014:  call       int32 [mscorlib]System.Console::Read()
  IL_0019:  pop
  IL_001a:  ret
} // end of method Program::Main

It appears the C# looks in current name space first

In this example the IL is

.method public hidebysig static void  Main(string[] args) cil managed
{
  .entrypoint
  // Code size       27 (0x1b)
  .maxstack  8
  IL_0000:  nop
  IL_0001:  ldc.i4.0
  IL_0002:  ldc.i4.s   10
  IL_0004:  call       class [mscorlib]System.Collections.Generic.IEnumerable`1<int32> [System.Core]System.Linq.Enumerable::Range(int32,
                                                                                                                                  int32)
  IL_0009:  call       int32 Test.Extensions::Count<int32>(class [mscorlib]System.Collections.Generic.IEnumerable`1<!!0>)
  IL_000e:  call       void [mscorlib]System.Console::WriteLine(int32)
  IL_0013:  nop
  IL_0014:  call       int32 [mscorlib]System.Console::Read()
  IL_0019:  pop
  IL_001a:  ret
} // end of method Program::Main

If I move the main method into the another namespace (XXX) in this case the compiler resolves the method to the System.Linq version

namespace Test
{
    public static class Extensions
    {
        public static int Count<TSource>(this IEnumerable<TSource> source)
        {
            return -1; //evil code
        }
    }

}

namespace XXX{

    public static class Program
    {
        public static void Main(string[] args)
        {
            Console.WriteLine(Enumerable.Range(0, 10).Count());   // -1, evil code works
            Console.Read();
        }
   }
}


.method public hidebysig static void  Main(string[] args) cil managed
{
  .entrypoint
  // Code size       27 (0x1b)
  .maxstack  8
  IL_0000:  nop
  IL_0001:  ldc.i4.0
  IL_0002:  ldc.i4.s   10
  IL_0004:  call       class [mscorlib]System.Collections.Generic.IEnumerable`1<int32> [System.Core]System.Linq.Enumerable::Range(int32,
                                                                                                                                  int32)
  IL_0009:  call       int32 [System.Core]System.Linq.Enumerable::Count<int32>(class [mscorlib]System.Collections.Generic.IEnumerable`1<!!0>)
  IL_000e:  call       void [mscorlib]System.Console::WriteLine(int32)
  IL_0013:  nop
  IL_0014:  call       int32 [mscorlib]System.Console::Read()
  IL_0019:  pop
  IL_001a:  ret
} // end of method Program::Main

To explicitly use your method in the latter example you use

namespace XXX{
    using Test;
    public static class Program
    {
        public static void Main(string[] args)
        {
            Console.WriteLine(Enumerable.Range(0, 10).Count());   // -1, evil code works
            Console.Read();
        }

    }
}

.method public hidebysig static void  Main(string[] args) cil managed
{
  .entrypoint
  // Code size       27 (0x1b)
  .maxstack  8
  IL_0000:  nop
  IL_0001:  ldc.i4.0
  IL_0002:  ldc.i4.s   10
  IL_0004:  call       class [mscorlib]System.Collections.Generic.IEnumerable`1<int32> [System.Core]System.Linq.Enumerable::Range(int32,
                                                                                                                                  int32)
  IL_0009:  call       int32 Test.Extensions::Count<int32>(class [mscorlib]System.Collections.Generic.IEnumerable`1<!!0>)
  IL_000e:  call       void [mscorlib]System.Console::WriteLine(int32)
  IL_0013:  nop
  IL_0014:  call       int32 [mscorlib]System.Console::Read()
  IL_0019:  pop
  IL_001a:  ret
} // end of method Program::Main
姜生凉生 2024-10-04 16:41:12

如果您创建一个新类并将 using 添加到两个命名空间,然后使用在两个命名空间中定义的方法,则错误应该会再次出现。

扩展方法是通过命名空间来区分的,而不是通过声明它们的静态类来区分的。如果在同一命名空间中定义了两个扩展方法,则编译器可以知道采用哪两个方法。

If you create a new class and add usings to both namespaces and then use your method that is defined in both namespaces, the error should be there again.

Extension methods are distinguish by namespaces, not by the static classes they are declared in. The compiler can know which one two take if two extension methods are defined in the same namespace.

嗼ふ静 2024-10-04 16:41:12

我认为,您正在编写两个具有相同重载的方法,这违反了 OOP 的原则。

如果扩展方法与您使用的扩展方法位于相同的命名空间范围内,那么如果相同的扩展方法存在于 System.Core.dll 中,则它将优先于 System.Core.dll 方法(因为它是在使用位置找到的最接近的重载)。两个命名空间。

为了证明这一点,请将扩展方法名称更改为 MyCount1(...) & MyCount2(...) ,那么它应该适合你。

I think, you are writing Two methods with the same overload which is against the principles of OOPs.

If extension method is within the same namespace scope as that of your usage, then it will take precedence(since it is the closest overload found at the place of usage) over the System.Core.dll one, if the same extension methods exists in both namespaces.

To prove this, change your extension method name to MyCount1(...) & MyCount2(...) , then it should work for you.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文