dotnet / roslyn

The Roslyn .NET compiler provides C# and Visual Basic languages with rich code analysis APIs.
https://docs.microsoft.com/dotnet/csharp/roslyn-sdk/
MIT License
18.92k stars 4.01k forks source link

Inconsistent behavior with dynamic and void returning functions #59132

Closed Grauenwolf closed 2 years ago

Grauenwolf commented 2 years ago

Version Used:

.NET 6

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net6.0</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
  </PropertyGroup>

</Project>

Steps to Reproduce:

// See https://aka.ms/new-console-template for more information
Console.WriteLine("Hello, World!");

var obj = new Tryme();
obj.bar();

void foo2(object x) { }

void bar2()
{
    object a = null;
    var b = foo2((dynamic)a); // Error  CS0815  Cannot assign void to an implicitly-typed variable
}

class Tryme
{
    void foo(object x) { }

    public void bar()
    {
        object a = null;
        var b = foo((dynamic)a); // no compiler error here
    }
}

Expected Behavior:

Function bar should have a compiler error just like bar2.

Actual Behavior:

Runtime error instead of compiler error. Microsoft.CSharp.RuntimeBinder.RuntimeBinderException: 'Cannot implicitly convert type 'void' to 'object''

333fred commented 2 years ago

This is by design in the language specification: see https://github.com/dotnet/csharpstandard/blob/draft-v6/standard/expressions.md#1165-compile-time-checking-of-dynamic-member-invocation for the specification for regular method invocations. There isn't yet a specification for local functions, but the compiler can make a very specific assumption about them it cannot make about regular methods: there is no overloading. To see how this comes into play, consider this example:

// In library lib
public class C
{
    public static void M(object o) => Console.WriteLine("Object");
}

// In a console app that depends on lib
C.M((dynamic)"");

In this version, there is only one M, and the code will always print Object. However, these are separate libraries, which means lib can be updated at runtime to have a new overload. Update your copy of lib to contain this instead:

public class C
{
    public static void M(object o) => Console.WriteLine("Object");
    public static void M(string s) => Console.WriteLine("String"); // New overload!
}

Rebuild lib, and then copy the output dll over to the console project and rerun your console app, without recompiling. You'll see that it now prints String instead of Object, as the DLR found the new method and determined it was a better match for the given argument. Since local functions can't be overloaded, the compiler doesn't have to consider this case, and can make stronger assumptions about the method group.

Arguably, the specification could call out methods that are from source specifically, and give an error in that case. However, I don't think that's a good idea, as you then have to start defining what "from source" actually means. Are two projects in the same solution both considered from source? I just demonstrated how that can actually change, even for projects in the same solution. And we don't like it when there's unintended magic that changes from within a project to without, and this would be such a case. So, we take a consistent rule and allow this case.