4 ms·
With NRTs this is a warning however for class-typed fields. Also `required` forces you to assign a value to it. Honestly I always thought struct init semantics
by neonsunset 2y ago
With NRTs this is a warning however for class-typed fields. Also `required` forces you to assign a value to it. Honestly I always thought struct init semantics in C# were slightly archaic and F# does it better by being strict in a way similar to Rust, so touching Go afterwards is a really big step back.
(i.e. you can author default constructor and field initializers for structs, in C#, the main "hole" remains the use of 'default(MyStruct)', but at least it is explicit and you get what you ask for)
- int_19h 2y agoThe real problem is downcasting (and virtual dispatch as a form of implicit downcast during dispatch). Consider: new Derived(); class Base { public Base() { Foo(); } public virtual void Foo() {} } class Derived: Base { public string s; // non-null! public Derived() { s = "abc"; } public override void Foo() { WriteLine(s == null); } } This will compile without warnings even with #nullable enable, and will print True at runtime. Note that this could also be rewritten without virtual methods by doing a downcast or pattern match: void Foo(Base b) { WriteLine(((Derived)b).s == null); } C++ solves this problem by making all virtual calls dispatch to Base until Base() completes (which involves swapping vtable pointers at runtime as needed), and then doing the same in reverse for destructors; it also makes the type of object Base for all other purposes like dynamic_cast. That is, in effect, in C++ the actual type of the object changes as it is constructed or destructed. Which is great from a theoretical point of view, but very counter-intuitive unless you understand the problem it's trying to solve. That aside, with respect to default(T), the other problem is arrays. If T is non-nullable and you ask for a new T[1], what should the value of the element be? C# lies and allows it to be null even for non-nullable reference types, which means that (new string[1])[0].ToString() is an NRE with no compiler diagnostic even with #nullable enabled. In C++, you actually cannot do this at all if T doesn't have a default constructor, but this then means that the language needs stuff like in-place new and explicit destructor calls to actually make a generic dynamic array possible to implement.
- neonsunset 2y agoI don't think C++ is good for comparison here - there's a huge elephant in the room of how much worse the overall experience of using C++ is both in terms of memory safety and convenience. It's not as common of a problem in C# to just initialize the array to the elements you need. Most arrays in regular code you will see will have their elements initialized or materialized from LINQ anyway. I agree that NRTs are "leaky" (and wish they did it better) but I do not run into the issues you do it seems and they are a massive productivity improvement, their static-analysis-based nature also helps with not dealing with some of the ceremony caused by e.g. Option<T> in Rust. On the snippet above - it will print true only when called inside the constructor, when the field has not been yet assigned to. It will print false otherwise. I do not see this as an issue, although it is indeed subtle.
- int_19h 2y agoC++ is the only similar language I know of that actually tries to tackle this problem. And to be clear, I'm not saying that nullability checking is a bad idea! On the contrary, it's great. It's especially great when the type system doesn't lie to you (i.e. if something being not nullable actually means that it can never be null), but even partial enforcement is better than nothing. However, the OP to whom I responded specifically complained about "idea of the zero values for uninitialized struct fields" in Go, and then in the same breath mentioned "language that works, like C#" - which is rather ironic given that C# does, in fact have this exact thing, and I tried to explain why it needs it.