dotnet / csharplang

The official repo for the design of the C# programming language
11.59k stars 1.03k forks source link

[Proposal] Required Properties #3630

Open 333fred opened 4 years ago

333fred commented 4 years ago

Required Properties

Specification: https://github.com/dotnet/csharplang/blob/master/proposals/csharp-11.0/required-members.md

Design meetings

https://github.com/dotnet/csharplang/blob/master/meetings/2020/LDM-2020-09-16.md#required-properties https://github.com/dotnet/csharplang/blob/master/meetings/2020/LDM-2020-12-07.md https://github.com/dotnet/csharplang/blob/master/meetings/2021/LDM-2021-01-11.md https://github.com/dotnet/csharplang/blob/master/meetings/2021/LDM-2021-03-03.md#required-members https://github.com/dotnet/csharplang/blob/main/meetings/2021/LDM-2021-10-25.md#required-members https://github.com/dotnet/csharplang/blob/main/meetings/2021/LDM-2021-12-15.md#required-parsing https://github.com/dotnet/csharplang/blob/main/meetings/2022/LDM-2022-03-21.md#open-question-in-required-members https://github.com/dotnet/csharplang/blob/main/meetings/2022/LDM-2022-03-23.md#open-questions-in-required-members https://github.com/dotnet/csharplang/blob/main/meetings/2022/LDM-2022-05-02.md#effect-of-setsrequiredmembers-on-nullable-analysis https://github.com/dotnet/csharplang/blob/main/meetings/2022/LDM-2022-11-30.md#revise-membernotnull-for-required

ericsampson commented 3 years ago

I know naming is bikesheddy and not that valuable, but I do like required (or reqd if people think the full text is too long to keep typing out as often as it'll get used)

ericsampson commented 3 years ago

Is there much consensus yet on it using a new qualifier (must init foo or reqd init foo) vs a new property modifier (required foo) ?

loligans commented 3 years ago

Nope nothing new in what I proposed. Thank you for informing me of the checked in proposals!

333fred commented 3 years ago

Is there much consensus yet on it using a new qualifier (must init foo or reqd init foo) vs a new property modifier (required foo) ?

Modifier, because we want to use it on fields as well.

ericsampson commented 3 years ago

Is there much consensus yet on it using a new qualifier (must init foo or reqd init foo) vs a new property modifier (required foo) ?

Modifier, because we want to use it on fields as well.

Then I'd vote for required spelled out too :)

danfma commented 3 years ago

I really love how C# is evolving but I don't like the idea of writing such long property definition just to make it required at the initialization. For me, less is more!

I agree with @DavidArno that the init by itself should be enough to declare something required at initialization in conjunction with the nullable types. However, I understand that when not using nullable type checking (which is required by some people yet) that could be a problem. Thus, if we can't change the init semantics, then I would go with that required or must modifiers.

ChristopherHaws commented 3 years ago

I wish there were compiler options like in TypeScript where you could enable "strict" mode to enable these types of features instead of them being additional keywords.

CyrusNajmabadi commented 3 years ago

We don't like dialects in the language :)

konrad-jamrozik commented 3 years ago

+1 for required instead of must, to avoid introducing synonyms. As in: "to make a property be required, add the keyword must".

jnm2 commented 3 years ago

Is there any prior example of a C# keyword being a contraction or abbreviation? reqd sounds like wrecked. Typing required will be just as easy since it should be req+<space> in the IDE. Seeing required is probably also easier to read.

KieranDevvs commented 3 years ago

Is there any prior example of a C# keyword being a contraction or abbreviation? reqd sounds like wrecked. Typing required will be just as easy since it should be req+<space> in the IDE. Seeing required is probably also easier to read.

There's bool for Boolean but I personally don't like req and don't see the harm in required.

bugproof commented 3 years ago

First of all, this keyword wouldn't be needed if nullable reference types were checked at compile time and giving errors instead of warnings if you didn't initialize non-nullable property. This really creates a lot of confusion about when to use nullable reference types and when to use required for experienced C# developers and for new developers.

I think

public required string UserName { get; init; }

should be implicit when NRTs are enabled and you just write this:

public string UserName { get; init; }

All these features seem half-baked

theunrepentantgeek commented 3 years ago

Nullable and required are independent and orthogonal concerns.

It's valid to have a nullable required property that's explicitly initialised to null, the choice is the developers.

bugproof commented 3 years ago

Nullable and required are independent and orthogonal concerns.

Yeah, I'm talking about non-nullable and required here because they seem the same. So why have both nullable reference types and required? Why can't I just enable nullable reference types and it would implicitly add required when I don't add ?? It's super confusing to me 🤷‍♂️

Consider I have <Nullable>enable</Nullable> in my .csproj and I add a record like this:

public record User
{
    public string UserName { get; init; }
}

So what difference will it make if I will add required there instead? Logically it should be required implicitly then. So now we have 4 ways of doing it:

public string UserName { get; init; } public required string UserName { get; init; } public required string? UserName { get; init; } public string? UserName { get; init; }

And what the difference is between the first two? Non-nullable/nullable is synonymous with required/non-required to me.

theunrepentantgeek commented 3 years ago

Let me rephrase what I said.

Whether a property is nullable (or not) and whether it's required (or not) are independent concerns - they're orthogonal matters that have nothing to do with each other.

Looking at your examples:

// May optionally be initialized by the consumer, may not be set to null
public string UserName { get; init; }

// Must be set by the consumer, may not be set to null
public required string UserName { get; init; }

// Must be set by the consumer, may be explicitly set to null 
public required string? UserName { get; init; }

// May optionally be initialized by the consumer to any value, including null
public string? UserName { get; init; }

For a more concrete example, consider this:

public required IRetryPolicy? RetryPolicy { get; init; }

I would write this if I wanted consuming code to be explicit about whether a retry policy was going to be used or not.

Seeing RetryPolicy = null in the middle of object initialization eliminates any confusion on the part of a reader about what's going on.

spydacarnage commented 3 years ago

I think their point for the first one

// May optionally be initialized by the consumer, may not be set to null
public string UserName { get; init; }

is that if it can't be set to null, but it is optional, what value does it have if the consumer doesn't set it? The answer is... null, which isn't allowed. I believe they're trying to say that in that scenario, the required is implied.

theunrepentantgeek commented 3 years ago

But the null is not implied - the existing constructor of the class gets to run first, and it gets to set whatever IT wants:

public class Person 
{
    public string FullName { get; init; }
    public string KnownAs { get; init; }
    public string FamilyName { get; init; }

    public class Person()
    {
        FullName = "";
        KnownAs = "";
        FamilyName = "";
    }
}

Conflating nullability and required'ness together would lead to really odd situations - such as requiring all three of FullName, KnownAs and FamilyName to be set every time, even though they already had valid non-null values.

spydacarnage commented 3 years ago

I think I'm getting confused with some of the early talk about this feature. I thought that the compiler errors were going to show at the caller when the object, after completing construction, isn't "complete".

So, in your example above, no errors, as the default (and only) constructor deals with all the initialisation.

However, consider:

public class Person 
{
    public string FullName { get; init; }
    public string KnownAs { get; init; }
    public string FamilyName { get; init; }

    public class Person()
    {
        FullName = "";
        FamilyName = "";
    }
}

var p = new Person();

That will create a scenario where KnownAs is undefined. As it currently stands, you get a warning in the class. The required tag moves that warning to the call-site (I thought). However, without the required tag, the intention of the non-null nature of the property is that it is still required.

Don't get me wrong - I have no problem with typing required to show further intent - I was just trying to see the issue from the direction of @bugproof

Richiban commented 3 years ago

It's worth laying out the possibilities. In a nullable context this:

public class C
{
    public string Prop { get; set; }
}

is no longer valid. Properties must either:

I don't necessarily agree that the last of those options should somehow be the default behaviour.

Euan-McVie commented 3 years ago

My understanding is that init only means allowed to be set in the initialiser. If your value was an 'int' then you would see different behaviours: public int Age { get; init; } - Will default to 0 if not set. public required int Age { get; init; } - Will raise the compiler warning at the caller. public int? Age{ get; init; } - Will default to null if not set. The only way to switch the compiler error on for an int is therefore to use an extra flag required to differentiate between behaviour.

I guess there is an argument that it could be handled by no longer defaulting the int to 0 unless it was explicitly set using public int Age { get; init; } = 0, but personally I feel that is changing currently expected behaviour of the primatives.

spydacarnage commented 3 years ago

And that is the killer piece of data I was forgetting. It can be implied (to a large degree) for reference types, but not value types.

There you go, @bugproof - there's your answer as to why required is, well, required...

magnusbakken commented 3 years ago

One thing I haven't seen addressed here is how required initialization will interact with the new() constraint.

Will this be an error?

class C<T> where T : new()
{
}

record R
{
    public required string S { get; init; }
}

class Program
{
    public static void Main() { new C<R>(); }
}
GREsau commented 3 years ago

The proposal specification includes this note at the end:

In order to ensure that we have design space in this area, we forbid required in interfaces, and forbid types with _required_memberlists from being substituted for type parameters constrained to new().

Which suggests that yes, that would be an error

magnusbakken commented 3 years ago

I missed that part. Good to know, thanks!

bugproof commented 3 years ago

Maybe if required keyword will be a thing then the compiler could allow something like this?

record Person(string Name);

// would be also possible to do
var person = new Person { Name = "Indiana Jones" };

// instead of
var person = new Person("Indiana Jones");

i.e. it would be possible to use parameterless constructor see https://github.com/dotnet/csharplang/discussions/4842

This would also be closer to how F# allows you to define records...

type Person = {Name: string}
333fred commented 3 years ago

We're not planning on having required changing anything about adding new constructors.

lucasteles commented 3 years ago

I think

public required string UserName { get; init; }

should be implicit when NRTs are enabled and you just write this:

public string UserName { get; init; }

I really agree with this

Serentty commented 3 years ago

I think public required string UserName { get; init; } should be implicit when NRTs are enabled and you just write this: public string UserName { get; init; }

I really agree with this

I think the “nullability and requiredness are orthogonal” position is unfortunate. It creates this situation where you have to keep piling on modifiers to get pit of success behaviour instead of the pit of failure.

Euan-McVie commented 3 years ago

I think public required string UserName { get; init; } should be implicit when NRTs are enabled and you just write this: public string UserName { get; init; }

I really agree with this

I think the “nullability and requiredness are orthogonal” position is unfortunate. It creates this situation where you have to keep piling on modifiers to get pit of success behaviour instead of the pit of failure.

My only concern would be that it would then mean an inconsistency between ref and value types Slightly forced example of both of these in the same class:

public string UserName { get; init; }
public required int Age { get; init; }

The value type needs it specified as otherwise the default of 0 is valid (which it might be in other use cases, just not for age). To me that looks slightly odd as both are required it is just that the ref string one is implicit if NRT is enabled. Also while I would love for NRT to be the default behaviour, it isn't (yet???), and so looking at this class I would have to go the csproj to check if it was turned on before knowing whether username is required or not. As someone who jumps about projects that are in different versions of .net etc. reviewing a PR to catch that the required is missing because the project doesn't have NRT enabled would be a real pain.

jods4 commented 3 years ago

My only concern would be that it would then mean an inconsistency between ref and value types

I know this doesn't justify everything but NRT and NVT are already vastly different beasts. They have all kind of discrepancies that C# justifies because of their different runtime representation. To me the inconsistency here is somehow "consistent" with existing inconsistencies.

In fact, you say you would find odd that one needs required and the other does not, but with no required modifiers do you find odd that one raises a C# warning and the other doesn't? The inconsistency is already there. I agree with previous comments that making uninitialized NRT required by default is more ergonomic than producing errors.

bugproof commented 3 years ago

https://www.reddit.com/r/csharp/comments/o56qaq/what_is_the_thinking_behind_records_not_requiring/

HaloFour commented 3 years ago

@bugproof

Was already asked here: #4857

SomeoneIsWorking commented 3 years ago

My vote is a keyword modifier on set/init. I kind of like required although req and must are ok, too. mustinit seems redundant.

public string Foo { get; required set; }
public string Bar { get; required init; }

What would required set be for?

canton7 commented 3 years ago

My vote is a keyword modifier on set/init. I kind of like required although req and must are ok, too. mustinit seems redundant.

public string Foo { get; required set; }
public string Bar { get; required init; }

What would required set be for?

It must be set during initialisation, but can be set thereafter, presumably.

spydacarnage commented 3 years ago

I think it has to go on the property itself, as required init; implies that it is required that an init block sets the property.

If the property is get in a constructor, it still meets the required status, but not in an init block

DavidArno commented 3 years ago

I don't see that at all. I'd expect required to mean it must be set during construction or initialisation. So if I call a constructor that sets it, then I do not need to set it in an initialisation block.

But having said that required get makes no sense, so public required int Age { get; init; } and public int Age { get; required init; } should be semantically identical and therefore where the required goes is really not important.

TonyValenti commented 3 years ago

I am of the opinion that required should go out front (toward the left) because it is a major aspect to a property and I believe having it left makes it more readily apparent.

But I am sure someone on here will state just the opposite. :-)

bugproof commented 3 years ago

TLDR: why can't required be default for all properties unless they have nullable type (e.g. int? or string?) or explicit =default if I want something to be default I don't see a problem explicitly adding = default

Also it's kinda funny how I get warnings that NRT is not initialized anywhere but I don't get that warnings for int when it's not int?

image

Obviously to get rid of that warning I'd have to initialize it from constructor or suppress the warning with null forgiving operator = null! but I couldn't guarantee that it will be assigned by the person using the record/class.

When there are many properties like 10 I'd have to initialize them all. required will help with this, but I still kinda wonder why it's not the default behaviour when something is not nullable... I think there should be a compiler option to make required implicit for all non nullable types (types without ? postfix).

When I write int or string I obviously don't want it to be null nor implicitly default when creating the object either by constructor or initialization syntax. I think when I want something to be default it should be explicitly specified.

Why force us to write public required string Name { get; init; } when NRTs are enabled? Same goes for ints... If the type is int and not int? I want it to be explicitly initialized most of the time...

Logically thinking this is how it should look like:

int - implicitly required unless you set it to = default int? - not required string - implicitly required string? - not required

I really don't want to write

required everytime when the type alone says if it is required or not

This is how I see required keyword to be honest:

So now when I enable NRT and type public string Name { get; init; } I will still get a warning that I have to initialize it from constructor or explicitly add required.

HaloFour commented 3 years ago

@bugproof

TLDR: why can't required be default for all properties unless they have nullable type

Because that would break an enormous amount of code and would most likely not what the author of that code intended.

Also it's kinda funny how I get warnings that NRT is not initialized anywhere but I don't get that warnings for int when it's not int?

Because the implicit default value of int doesn't lead to NullReferenceException. Again, such a change would break an enormous amount of existing code.

but I still kinda wonder why it's not the default behaviour when something is not nullable...

As stated several times required properties have nothing to do with avoiding null or default values for those properties. You can explicitly set a required property to default (or null). A nullable property can have a default non-null value. These are orthogonal concerns.

If your intent is to require that the consumer of your class set a property during initialization then that is what required is for. That is not dependent on the type of the property nor can it be made the default behavior in arbitrary circumstances without causing breaking changes and resulting in developers who don't want that property to be required lacking a means to communicate that without another language feature.

bugproof commented 3 years ago

@HaloFour

Because that would break an enormous amount of code and would most likely not what the author of that code intended.

It wouldn't if it could be optionally enabled like NRTs. Some .csproj tag like <ImplicitRequired>enable</ImplicitRequired> and everyone would be happy. The resulting bytecode would be the same anyway. It would only allow you to prevent mistakes in your own code. It wouldn't have any impact on existing code. If code generators could do it I would happily write such generator myself.

spydacarnage commented 3 years ago

Some .csproj tag like <ImplicitRequired>enable</ImplicitRequired> and everyone would be happy.

Because that would mean that a compiler flag would modify the meaning of your code, which the language team are explicit about not wanting to do.

(and before you say it, NRT does not modify the meaning of your code - it enables you to write code you couldn't without it, just like unsafe)

bugproof commented 3 years ago

Because that would mean that a compiler flag would modify the meaning of your code, which the language team are explicit about not wanting to do.

Why not? It would only make things better and less error prone. If it's not possible then maybe it's possible to write some roslyn analyzer that errors when you don't assign everything that's non nullable?

HaloFour commented 3 years ago

@bugproof

If it's not possible then maybe it's possible to write some roslyn analyzer that errors when you don't assign everything that's non nullable?

You're free to do that.

spydacarnage commented 3 years ago

Why not? It would only make things better and less error prone.

Quite the opposite - because this compiler flag could be in a CLI switch instead of a .csproj file, a person who has your code doesn't know whether it should be compiled with the flag or without it, and the final program would be different depending on which option you choose. And that will create more errors than fix, in my mind.

magnusbakken commented 3 years ago

You would have to ask yourself whether you would want to enforce this:

public class C
{
    public bool B { get; init; } = false;
}

...or if this is also okay:

public class C
{
    public bool B { get; init; }

    public C()
    {
        B = false;
    }
}

If you want only the former to be allowed, it's easy enough. If you want to allow the second one as well it gets much thornier. Here's an old Eric Lippert post about why the compiler doesn't enforce initialization: https://stackoverflow.com/a/30820127/1696533

saithis commented 3 years ago

Just a thought: How about required class and required record to make all properties required for that class/record?

DavidArno commented 3 years ago

Just a thought: How about required class and required record to make all properties required for that class/record?

I like it. required sealed record Foo is still a longwinded way of getting to a "best practice" starting point for a new type, but it's better than having to put required against every parameter.

bugproof commented 3 years ago

Just a thought: How about required class and required record to make all properties required for that class/record?

And what if property was nullable? Would it also be required or you would be forced to assign null to every property like that?

KieranDevvs commented 3 years ago

Just a thought: How about required class and required record to make all properties required for that class/record?

And what if property was nullable? Would it also be required or you would be forced to assign null to every property like that?

You'd be forced to assign a value. It just so happens null is a valid option.

DavidArno commented 3 years ago

@bugproof, I agree with @KieranDevvs here. Whether it's nullable or not is irrelevant if you mark it required, Mark it thus and you are then required to specify a value, even if that value is null. Marking it null-nullable implies it's required and you'll get a warning that it needs defaulting if you do not mark it required. I don't see the opposite (nullable and required) as an issue.