csharplang/meetings/2014/LDM-2014-10-01.md
Nick Schonning 6cd82c21ed fix: MD033/no-inline-html
Inline HTML get swallowed in MD and HTML rendering
2019-05-25 01:31:46 -04:00

12 KiB

There were two agenda items...

  1. Assignment to readonly autoprops in constructors (we fleshed out details)
  2. A new compiler warning to prevent outsiders from implementing your interface? (no, leave this to analyzers)

Assignment to readonly autoprops in constructors

public struct S {
   public int x {get;}
   public int y {get; set;}
   public Z z {get;}

   public S() {
      x = 15;
      y = 23;
      z.z1 = 1;
   }
}

public struct Z { int z1; }

What are the rules under which assignments to autoprops are allowed in constructors?

Absolute We can't be more permissive in what we allow with readonly autoprops than we are with readonly fields, because this would break PEVerify. (Incidentally, PEVerify doesn't check definite assignment in the constructor of a struct; that's solely a C# language thing).

Overall principle When reading/writing to an autoprop, do we go via the accessor (if there is one) or do we bypass it (if there is one) and access the underlying field directly? Option1: language semantics say the accessor is used, and codegen uses it. Option2: in an appropriate constructor, when there is a "final" autoprop (either non-virtual, or virtual in a sealed class), access to an autoprop means an access to the underlying field. This meaning is used for definite assignment, and for codegen. Note that it is semantically visible whether we read from an underlying field vs through an accessor, e.g. in int c { [CodeSecurity] get;} Resolution: Option1. Under Option2, if you set a breakpoint on the getter of an autoprop, gets of it would not hit the breakpoint if they were called in the constructor which is weird. Also it would be weird that making the class sealed or the autoprop non-virtual would have this subtle change. And things like Postsharper wouldn't be able to inject. All round Option2 is weird and Option1 is clean and expected.

Definite Assignment. Within an appropriate constructor, what exactly are the rules for definite assignment? Currently if you try to read a property before all fields have been assigned then it says CS0188 'this' cannot be used before all fields are assignment, but reading a field is allowed so long as merely that field has been assigned. More precisely, within an appropriate constructor, for purposes of definite assignment analysis, when does access of the autoprop behave as if it's an access of the backing field? Option1: never Option2: Only in case of writes to readonly autoprops Option3: In the case of writes to all autoprops Option4: In the case of reads and writes to all autoprops Resolution: Option4. This is the most helpful to developers. You might wonder what happens if it's a virtual autoprop and someone overrides getter or setter in derived types in such a way that would violate the definite assignment assumptions. But for structs there won't be derived types, and for classes the semantics say that all fields are assigned to default(.) so there's no difference.

Piecewise initialization of structs. In the code above, do we allow z.z1 = 15 to assign to the field of a readonly struct autoprop? Option1: Yes by threating access to "z" for purposes of definite assignment as an access of the underlying field. Option2: No because in z.z1 the read of z happens via the accessor as per the principle above, and thus returns an rvalue, and hence assignment to z.z1 can't work. Instead you will have to write z = new Z(...). Resolution: Option2. If we went with Option1, then readonly autoprops would end up being more expressive than settable autoprops which would be odd! Note that in VB you can still write _z.z1 = 15 if you do want piecewise assignment.

Virtual. What should happen if the readonly autoprop is virtual, and its getter is overridden in a derived class? Resolution: All reads of the autoprop go via its accessor, as is already the case.

Semantic model. In the line x = 15 what should the Roslyn semantic model APIs say for the symbol x ? Resolution: they refer to the property x. Not the backing field. (Under the hood of the compiler, during lowering, if in an appropriate constructor, for write purposes, it is implicitly transformed into a reference to the backing field). More specifically, for access to an autoprop in the constructor,

  1. It should bind to the property, but the property should be treated as a readable+writable (for purposes of what's allowed) in the case of a readonly autoprop.
  2. The definite assignment behavior should be as if directly accessing the backing field.
  3. It should code gen to the property accessor (if one exists) or a field access (if not).

Out/ref arguments in C#. Can you pass a readonly autoprop as an out/ref argument in C#? Resolution: No. For readonly autoprops passed as ref arguments, that wouldn't obey the principle that access to the prop goes via its accessor. For passing readonly autoprops as out arguments with the hope that it writes to the underlying field, that wouldn't obey the principle that we bind to the property rather than the backing field. For writeonly autoprops, they don't exist because they're not useful.

Static readonly autoprops Should everything we've written also work for static readonly autoprops? Resolution: Yes. Note there's currently a bug in the native compiler (fixed in Dev14) where the static constructor of a type G<T> is able to initialize static readonly fields in specializations of G e.g. G<T>.x=15;. The CLR does indeed maintain separate storage locations for each static readonly fields, so G<int>.g and G<string>.g are different variables. (The native compiler's bug where the static constructor of G could assign to all of them resulted in unverifiable code).

VB rules in initializers as well as constructors. VB initializers are allowed to refer to other members of a class, and VB initializers are all executed during construction time. Should everything we've said about behavior in C# constructors also apply to behavior in VB initializers? Resolution: Yes.

VB copyback for ByRef parameters. In VB, when you pass an argument to a ByRef parameter, then either it passes it as an lvalue (if the argument was a local variable or field or similar) or it uses "copy-in to a temporary then invoke the method then copy-out from the temporary" (if the argument was a property), or it uses "copy-in to a temporary then invoke the method then ignore the output" (if the argument was an rvalue or a constant). What should happen when you pass a readonly autoprop to a ByRef parameter? Option1: Emit a compile-time error because copyback is mysterious and bites you in mysterious ways, and this new way is even more mysterious than what was there before. Option2: Within the constructor/initializers, copy-in by reading via the accessor, and copy-back by writing to the underlying field. Elsewhere, copy-in with no copy-out. Also, just as happens with readonly fields, emit an error if assignment to a readonly autoprop happens in a lambda in a constructor (see code example below) Resolution: Option2. Exactly has happens today for readonly fields. Note incidentally that passing a readonly autoprop to a ByRef parameter will have one behavior in the constructor and initializers (it will do the copy-back), and will silently have different behavior elsewhere (it won't do any copy-back). This too is already the case with readonly fields. On a separate note, developers would like to have feedback in some cases (not constants or COM) where copyback in a ByRef argument isn't done. But that's not a question for the language design meeting.

VB copyin for writeonly autoprops. VB tentatively has writeonly autoprops for symmetry, even though they're not useful. What should happen when you pass a writeonly autoprop as a ByRef argument? Resolution: Yuck. This is a stupid corner case. Notionally the correct thing is to read from the backing field, and write via the setter. But if it's easier to just remove support for writeonly autoprops, then do that.

Class C
    ReadOnly x As Integer = 15

    Public Sub New()
        f(x)
        Dim lambda = Sub()
                         f(x) ' error BC36602: 'ReadOnly' variable
                         ' cannot be the target of an assignment in a lambda expression
                         ' inside a constructor.
                     End Sub
    End Sub
    Shared Sub f(ByRef x As Integer)
        x = 23
    End Sub
End Class

We discussed a potential new error message in the compiler.

Scenario: Roslyn ships with ISymbol interface. In a future release it wants to add additional members to the interface. But this will break anyone who implemented ISymbol in its current form. Therefore it would be good to have a way to prevent anyone else from implementing ISymbol. That would allow us to add members without breaking people.

Is this scenario widespread? Presumably, but we don't have data and haven't heard asks for it. There are a number of workarounds today. Some workarounds provide solid code guarantees. Other workarounds provide "suggestions" or "encouragements" that might be enough for us to feel comfortable breaking people who took dependencies where we told them not to.

Counter-scenario: Nevertheless, I want to MOCK types. I want to construct a mock ISymbol myself maybe using MOQ, and pass it to my functions which take in an ISymbol, for testing purposes. I still want to be able to do this. (Note: MOQ will automatically update whenever we add new members to ISymbol, so users of it won't be broken).

Workarounds

  1. Ignore the problem and just break people.
  2. Like COM, solve it by adding new incremental interfaces ISymbol2 with the additional members. As Adam Speight notes below, you can make ISymbol2 inherit from ISymbol.
  3. Instead of interfaces, use abstract classes with internal constructors. Or abstract classes but never add abstract methods to it; only virtual methods.
  4. Write documentation for the interface, on MSDN or in XML Doc-Comments, that say "Internal class only; do not implement it". We see this for instance on ICorThreadpool.
  5. Declare a method on the interface which has an internal type in its signature. The CLR allows this but the language doesn't so it would have to be authored in IL. Every type which implements the interface would have to provide an implementation of that method.
  6. Write run-time checks at the public entry points of key Roslyn methods that take in an ISymbol, and throw if the object given was implemented in the wrong assembly.
  7. Write a Roslyn analyzer which is deployed by the same NuGet package that contains the definition of ISymbol, and have this analyzer warn if you're trying to implement the interface. This analyzer could be part of Roslyn, or it could be an independent third-party analyzer used by many libraries.

Proposal: Have the compiler recognize a new attribute. Given the following code

[System.Runtime.CompilerServices.InternalImplementationOnly] interface I<...> {...}

it should be a compile-time warning for a type to implement that interface, directly or indirectly, unless the class is in the same assembly as "I" or is in one of its InternalsVisibleTo friends. It will also be a compile-time error for an interface to inherit from the interface in the same way. Also, we might ask for the .NET Framework team to add this attribute in the same place as System.Runtime.CompilerServices.ExtensionAttribute, and CallerMemberNameAttribute. But doing it isn't necessary since the compiler will recognize any attribute with that exact fully-qualified name and the appropriate (empty) constructor.

Note that this rule would not be cast-iron, since it won't have CLR enforcement. It would still be possible to bypass it by writing IL by hand, or by compiling with an older compiler. But we're not looking for cast-iron. We're just looking for discouragement strong enough to allow us to add members to ISymbol in the future. (In the case of ISymbol, it's very likely that people will be using Roslyn to compile code relating to ISymbol, but that doesn't apply to other libraries).

Resolution: Workaround #7 is a better option than adding this proposal to the language.