I am using a type provider (FSharp.Data.Npgsql) that generates a bunch of classes structured roughly like this:
class Table {
class Columns {
class MyId {
}
public MyId MyId { get; }
}
public Columns Columns { get; }
}
and I need to access the string "MyId" in a type-safe way. So my code has a bunch of lines like the following
let n = nameof Table.Columns.MyId
With the 3.1.402 SDK, that expression compiles succesfully (and resolves to "MyId").
When compiling with the 5.0.100-rc.1.20452.10 SDK, it outputs the error error FS0806: Property 'Columns' is not static.
Repro steps
The quickest repro is probably to install the FSharp.Data.Npgsql package in a new project and then access any column of any table.
Expected behavior
Either the following would work:
nameof can read the name of (nested) instance properties through the type namenameof's target expression prioritizes the nested type over the instance propertiesThese behaviours would preserve the .NET Core 3.1 behaviour.
It might be desirable to have a manual option to indicate which symbol the user is referring to. However, nameof is one of the few language constructs that can take either a type or a value as its argument, so it's kind of a niche scenario.
Known workarounds
Wrapping the receiver type in Unchecked.defaultof<Table> works (thanks @TIHan).
Defining a type alias doesn't work, since nameof simply returned the name of the type alias.
Using reflection via typeof<>.Name can work in some cases, although it's not equivalent to a compile-time check, but it doesn't work in mine since the TP type names are not available at runtime.
Related information
Provide any related information (optional):
I believe this was a misstep in the work to polish off nameof and ensure that we couldn't access instant members as if they were static. Looks like there is a valid use case for this if the type is provided. @dsyme @TIHan FYI
I think this is by-design. The way to do this now is by using Unchecked.defaultof:
let n = nameof Unchecked.defaultof<Table>.Columns.MyId
This is safe because nameof is constant.
I don't think this is by design. I think it's a shortcoming in the design. When working with TPs you don't really instantiate the types you work with, you just access them.
@TIHan
I don't think it's by design either, although unlike @cartermp I think TPs aren't the issue.
Rather, C# nameof doesn't ask you to use default() for instance members:
nameof(String.Length); // "Length"
But Unchecked.defaultof<'t> is a workaround, at least, thanks.. I'll add it to the original post.
Note that in your example with String, it is bu design. It's not a goal to allow "static" access to instance members. The reason why I think this is worth considering for TPs is that you don't really new up the provided types like you do other objects.
Oh, I think I understand now. From the RFC:
nameof(M) in C# can resolve to an instance member. F# doesn't support unqualified resolution of instance members, so a dummy this argument is needed, e.g. nameof(Unchecked.defaultof
.M) or nameof(this.M) etc.
And some discussion specifically about the instance issue from last year.
So, assuming this specification wasn't added at a later point, I'm guessing the code only worked under .NET 3.1 because nameof used to resolve Table.Columns as the type Columns which happened to be nested under Table, not as the instance property Columns of the type Table.
Going back to my original post:
Expected behaviour:
Either the following would work:
- nameof can read the name of (nested) instance properties through the type name
- nameof's target expression prioritizes the nested type over the instance properties
If I understand the original issue correctly, the different behavior between Core and NET 5 comes from the change in priority of resolving to a nested type vs a member. I'd assume that a type, nested or not, takes precedence over members of the same name.
If such priority would be given, I believe that'd solve the main issue reported here.
Afaik, class/static members by itself aren't currently an issue: nameof String.Empty works (whereas instance members require a, potentially erasable or "null', instance).