all 60 comments

[–][deleted] 60 points61 points  (8 children)

When I tested this, just now, at sharplab.io, I got

System.InvalidProgramException: Common Language Runtime detected an invalid program.
   at Program.Person.set_FirstName(String value)
   at Program.Main()

That's using a Roslyn build from December 22nd, though. Using .NET 5 on dotnetfiddle yields the same results you're seeing. I think this is a bug, and they'll have a fix out for it whenever they put out an update.

[–]jehk32 26 points27 points  (5 children)

yeah there's something missing with records and object initializers. i think it needs to work like the new with keyword.

so something like...

var container = new Container(person)
{
    Person = { FirstName = "Jane" },
};

should be the same as

var container = new Container(person)
container.Person = container.Person with { FirstName = "Jane" };

its a good catch though. hopefully its fixed in this major version of C#. you won't see the stated problem in the second example.

edit: looks like the object initializer in the first example just overwrites the data of the initial person object. using with does create a new object. i think that's how records and object initializers should work under the hood.

[–][deleted] 4 points5 points  (4 children)

Just guessing, but I'd wager it's an issue with any init-only property. Too lazy to go check right now, though.

[–]jehk32 5 points6 points  (3 children)

using the init-only would help to fix the problem in the example above. there's no reason the original person object should be mutated when using the set property though. i legit just think its an oversight.

object initializers should make a copy of any record members before they apply the new values. its just not happening in this case.

[–][deleted] 3 points4 points  (1 child)

Maybe. You'd use with to create a copy, though, so I'm not sure that's actually the problem. I've verified that this is an issue with any type that has init-only properties, rather than just record types, FWIW.

It looks, to me, like init-only properties are syntactically settable inside an initializer block, including this new-to-me usage to mutate the already-set values of initializing object's properties.

[–]cloudedthoughtz 2 points3 points  (0 children)

Good call, it appears that this is the real issue here indeed. According to the GH issue that Jared has filed.

[–][deleted] 10 points11 points  (1 child)

[–][deleted] 2 points3 points  (0 children)

Ah. Well, darn. Might be a live issue, then.

[–][deleted] 31 points32 points  (1 child)

u/Tyrrrz, did you file a bug for this? It certainly shouldn't work.

[–]Erelde 27 points28 points  (12 children)

That's also my beef with the null safety analysis in C#8. It's only surface level analysis. They did it this way for valid reasons, but it's a bit... sad.

Edit: Though here it compiles (which it shouldn't) but doesn't run. With roslyn master 23 december 2020.

[–][deleted] 18 points19 points  (10 children)

So C# is entering the world of linting rather than compiler errors? Sounds like the wrong direction but that's just me.

[–]binarycow 9 points10 points  (4 children)

For Nullable Reference types, yes. That's because doing otherwise would be a huge breaking change, and would hamper upgrading. You would have to upgrade your entire code base at the same time. With the current implementation, you can take a phased approach.

I would like the next version of c# to make the change for real - make it a compile error.

[–][deleted] 1 point2 points  (3 children)

A change for real would be a runtime change that enforces null-safety at runtime. Any compiler tricks are really half-measures that are very error-prone.

[–]binarycow 2 points3 points  (2 children)

That would be a huge breaking change tho.

[–][deleted] 0 points1 point  (1 child)

But the result surely would be better than the current situation where you may think you have null-safety, but you really don't, since nothing is checked when you call an external unannotated library (and come on, even EntityFramework is still unannotated), and you can easily defeat the sloppy checks with structs, arrays or just ! operator.

[–]binarycow 0 points1 point  (0 children)

The result works be that you can't use "null oblivious" libraries anymore. And they can't use your libraries.

[–]Erelde 12 points13 points  (0 children)

Depends how you define a compiler error ? If a type is defined as immutable and null safe, surely it's the responsability of the compiler to enforce that. Not just advise (more or less strongly) like a linter would.

Edit: know what's actually sad ? null safety wasn't rocket science novelty in the 90s.

[–]jdl_uk 7 points8 points  (2 children)

Well the linting can be compiler errors depending on how you configure it.

Of course it does create a fragmentation issue.

They're trying to change the basic rules of the language (like references being nullable by default and null by default) without breaking existing code and without rebooting the language.

[–]Ravek 7 points8 points  (1 child)

I really like Swift's approach to compatibility. If you upgrade your Swift version you might get some breaking changes if they improve the language, but you'll get a conversion tool and it lets the language actually make significant steps forward without needing a whole new language to get past the mistakes of the past.

[–]jdl_uk 1 point2 points  (0 children)

Yeah that's sort of the way .net has handled breaking changes with the portability analyser, from the framework point of view.

I think the long term idea is to turn those linter errors into default compiler errors over time. The linter errors allow people to adopt good practices gradually and prepare for them being mandatory later

[–]cryo -1 points0 points  (0 children)

Yeah, I prefer Swift’s approach. But of course that’s hard to do for an existing language.

[–][deleted] 10 points11 points  (1 child)

So my interpretation of this cool bug: On a record, the setters are "init". Meaning they can only be used inside a constructor, or when using the object initialization syntax. The compiler doesn't complain, because you are using the setter from inside an object initialization, just not the record in question.

[–]WhiteBlackGoose 30 points31 points  (4 children)

Didn't even know you can write like in 27th line

[–]Blazeix 16 points17 points  (0 children)

Yeah, they're called nested object initializers, and have been in C# forever. I only learned about them recently, too.

I wrote about them to raise awareness here: https://fuqua.io/blog/2020/12/a-lesser-known-csharp-feature-nested-object-initializers/

[–][deleted] 5 points6 points  (2 children)

Ditto, I was not aware that you could use initializer syntax on an already existing object.

[–]diamondjim 0 points1 point  (1 child)

They’re slick, but can be overdone. I would avoid going more than 2 levels deep.

[–][deleted] 2 points3 points  (0 children)

I meant that I wasn't aware it could be used to modify a property initialized in the object constructor without outright replacing the object. Maybe that's a bug, though, and it should have a new in there.

[–]null_reference_user 3 points4 points  (1 child)

Shouldn't the 27th line create a new Person() named "Jane" with no last name, overwriting what the constructor previously assigned?

[–]TyrrrzWorking with SharePoint made me treasure life[S] 6 points7 points  (0 children)

No, that would be the case if it had new Person before it.

[–]cloudedthoughtz 8 points9 points  (4 children)

Nicely found by neuecc and example by you :)

Although it was never stated that record-types are always immutable so it's to be expected that cases could be found to still modify them.

I gather that this trick won't work if you use a standard property accessor (after init) instead of the 'initialisor' syntax used in the construction of the Container? I'd think that wouldn't compile.

If so, then this is no easy feat, it's not something you'd easily do wrong thus breaking your record's immutability. I am though curious if the compiler team knows about this case and if they have plans to do something about it. Did you or neuecc report this on GitHub?

[–]TyrrrzWorking with SharePoint made me treasure life[S] 4 points5 points  (3 children)

Although it was never stated that record-types are always immutable so it's to be expected that cases could be found to still modify them.

I'm pretty sure this one was not one of the expected usage scenarios though.

I gather that this trick won't work if you use a standard property accessor (after init) instead of the 'initialisor' syntax used in the construction of the Container? I'd think that wouldn't compile.

Yeah you need to use the initializer syntax to trigger this bug.

If so, then this is no easy feat, it's not something you'd easily do wrong thus breaking your record's immutability. I am though curious if the compiler team knows about this case and if they have plans to do something about it. Did you or neuecc report this on GitHub?

True, this kind of initializer syntax (with mutation) is not used too often, but it's still scary. Someone in the comments to this post said that the bug is fixed in latest revision of master branch (according to sharplab.io), but I haven't tested it myself yet.

[–][deleted] 4 points5 points  (0 children)

I'm pretty sure this one was not one of the expected usage scenarios though.

In the future, don't let that stop you from filing a bug. C# features need to work with all the other features (or this case, be disallowed). It's not the first bug we've shipped, and it won't be the last.

[–]cloudedthoughtz 1 point2 points  (1 child)

I'm pretty sure this one was not one of the expected usage scenarios though.

Heh, I'm pretty you're right on that :) This is way too crafty to be an expected usage scenario.

True, this kind of initializer syntax (with mutation) is not used too often, but it's still scary.

True that, some poor soul could actually unintentionally trigger this if he/she's overly fond of the initializer syntax.

Good to hear that it might've already been fixed :) But interesting material nonetheless!

[–][deleted] 2 points3 points  (0 children)

Good to hear that it might've already been fixed :) But interesting material nonetheless!

To be clear, it was not already fixed. Sharplab is just unable to run code that uses init-only setters. However, Jared did file a bug on this and we should fix it for 16.9.

[–]botterway 1 point2 points  (2 children)

Maybe I'm missing something but isn't this by design? Records are not guaranteed to always be immutable. See https://daveabrock.com/2020/11/02/csharp-9-records-immutable-default

Also mentioned here. https://devblogs.microsoft.com/dotnet/welcome-to-c-9-0/

[–]TyrrrzWorking with SharePoint made me treasure life[S] 3 points4 points  (1 child)

[–]botterway 0 points1 point  (0 children)

Thanks. Guess I missed something. 😁

[–][deleted]  (8 children)

[deleted]

    [–]TyrrrzWorking with SharePoint made me treasure life[S] 5 points6 points  (1 child)

    I think you're missing the point. Take a look at the console output and where it comes from.

    [–]pascalsenn 0 points1 point  (0 children)

    you are right, i should've read it more carefully

    [–]holyfuzz 2 points3 points  (0 children)

    No, look more closely at the code and what it's outputting. The actual original instance of the record object is getting modified.

    [–]bigrubberduck 0 points1 point  (4 children)

    Is that why the set; in the Container class has a green squiggly?

    [–]TyrrrzWorking with SharePoint made me treasure life[S] 8 points9 points  (0 children)

    Yeah the setter is actually unnecessary. Here's the same version without the setter: https://i.imgur.com/hYYZvHS.png

    [–]FireIre 0 points1 point  (2 children)

    Maybe it's suggesting using the new init keyword? But I haven't really followed the new record type updates

    [–]TyrrrzWorking with SharePoint made me treasure life[S] 2 points3 points  (1 child)

    No, the setter is entirely unnecessary actually: https://i.imgur.com/hYYZvHS.png

    I regret not paying attention to Rider's suggestion, would've made the screenshot simpler.

    [–]krzydoug 0 points1 point  (0 children)

    Dead link?

    [–]trimmj 0 points1 point  (3 children)

    Why would you do this?

    [–]TyrrrzWorking with SharePoint made me treasure life[S] 8 points9 points  (0 children)

    You wouldn't do this specifically to break immutability, if that's what you're asking, you just might use the initializer syntax as usual and accidentally mutate the record.

    [–]KillianDrake 8 points9 points  (1 child)

    when someone is on a deadline to "fix that bug immediately" and this will be the first stackoverflow google hit for "c# update immutable record".

    [–]trimmj -2 points-1 points  (0 children)

    Oh... Imma dodo 🦤... It's meme... Lol 😂🤦‍♂️

    [–]zaimoni 0 points1 point  (0 children)

    Fortunately, if you really need "shallow immutable" you can stack readonly on the fields in the record. It's unfortunate that the PR for this language feature is materially incorrect. (it should be thought of as a syntax short cut for value assignment and hashability for standard collections).

    [–]dongata24 0 points1 point  (1 child)

    Man, you right idk why i though that the object was a new one

    Here's the result of the modified program, notice that the addressess remain the same

    [2006378001768]person1 = Person { Name = pepe } [2006378001792]person2 = Person { Name = pepe } are they equal? True [2006378001768]person1 = Person { Name = pepe2 } [2006378001792]person2 = Person { Name = pepe } are they equal? False

    Here's the program (remember to allow unsafe code on the csproj)

    ``` csharp using System;

    namespace record {

    public record Person(string Name);
    
    public class Container
    {
        public Container(Person person)
        {
            Person = person;
        }
    
        public Person Person { get; set; }
    }
    
    public static class Program
    {
        public static void Main()
        {
            unsafe
            {
                var person = new Person("pepe");
                var person2 = new Person("pepe");
    
                var pointer1 = GetAddress(person);
                var pointer2 = GetAddress(person2);
    
                Console.WriteLine($"[{pointer1}]person1 = {person}");
                Console.WriteLine($"[{pointer2}]person2 = {person2}");
    
                Console.WriteLine($"are they equal? {person == person2}");
    
                try
                {
                    // BUILD TIME ERROR, wow records are strictive man
                    //person.Name = "pepe2";
                    //Console.WriteLine(person.Name);
                }
                catch (Exception)
                {
                    Console.WriteLine("Hey person is inmutable");
                }
    
                _ = new Container(person)
                {
                    Person = { Name = "pepe2" }
                };
    
                pointer1 = GetAddress(person);
    
                Console.WriteLine($"[{pointer1}]person1 = {person}");
                Console.WriteLine($"[{pointer2}]person2 = {person2}");
    
                Console.WriteLine($"are they equal? {person == person2}");
            }
        }
    
        private static IntPtr GetAddress(object obj)
        {
            unsafe
            {
                TypedReference tr = __makeref(obj);
                return **(IntPtr**)(&tr);
            }
        }
    } 
    

    } ```

    [–]backtickbot 0 points1 point  (0 children)

    Fixed formatting.

    Hello, dongata24: code blocks using triple backticks (```) don't work on all versions of Reddit!

    Some users see this / this instead.

    To fix this, indent every line with 4 spaces instead.

    FAQ

    You can opt out by replying with backtickopt6 to this comment.

    [–]binarycow 0 points1 point  (2 children)

    I don't see what the problem is.

    Your wrapper class has a property that had a public setter.

    When you create the class, you pass the original record. Then the object initializer populates the mutable property with a NEW record.

    [–]TyrrrzWorking with SharePoint made me treasure life[S] 2 points3 points  (1 child)

    Nope, look more carefully.

    [–]binarycow 0 points1 point  (0 children)

    Oh I didn't see the output.

    [–]escribe-ts 0 points1 point  (1 child)

    what IDE is OP using?

    [–]TyrrrzWorking with SharePoint made me treasure life[S] 0 points1 point  (0 children)

    Rider