In the 28th January meeting we decided the inits()
syntax is the one we're happiest with so far. There are likely multiple areas of concern about it, but one noteworthy concern is about the keyword that begins the "inits" clause.
A few reasons for concern about the keyword "inits":
- inits is VB-ish, in the way that VB says
Overrides
while C# saysoverride
. - inits strongly resembles init, but init has an unrelated meaning, which we may want to introduce independently to method signatures. (The proposed init modifier on a method enables it to call init accessors, while making the method only callable during construction.)
The biggest reason I can think of to separate inits
from init
is the following: A "helper" should not be limited to only being called during construction. If object instances are sometimes allocated from scratch and used, and other times are fetched from a pool and used, it may be desirable to call a helper both during and after construction.
Consider if we just used the init
keyword for this purpose, and for helpers either the init
or set
keywords, depending on whether the helper needs to call init
accessors.
public class Widget
{
public required string Name { get; set; }
public required int Id { get; init; }
public Widget(string name, int id)
// perhaps 'set()' would be disallowed on constructors because a constructor can only be called "during construction" anyway.
: init(Name = name, Id = id)
{
}
public Widget(string name)
: init(Name, Id)
{
SetName(name);
InitId();
}
// this "helper" can only be called during construction, and would likely be restricted to at most 'protected' accessibility.
protected void InitId()
: init(Id = 42)
{
}
// this "helper" can be called both before or after construction.
public void SetName(string name)
: set(Name = name)
{
}
// - empty 'init' clause permits us to set 'init' properties without requiring us to set any in particular.
protected void MaybeInitId()
: init() // perhaps optionally elide the parentheses? or even allow it to be used as a modifier instead of as a clause.
{
Id = 42;
}
}
A consequence of this is that it pushes users to use the init "clause" syntax even when they aren't interested in specifying which properties they are going to init. But it's also a little more "symmetrical" with the accessor declarations which treat "set" and "init" as mutually exclusive.
Alternatively: init
could be a standalone modifier on method signatures, and set()
could be the "only" way to specify a "set clause".
public class Widget
{
public required string Name { get; set; }
public required int Id { get; init; }
public Widget(string name)
: set(Name = name, Id)
{
InitId();
}
protected init void InitId()
: set(Id = 42)
{
}
protected init void MaybeInitId()
{
Id = 42;
}
}
I think I prefer the init()
/set()
scheme in the previous section, perhaps because
set
indicating that the method calls aninit
accessor "feels weird", versusinit
indicating that a method calls aset
accessor.- During construction we colloquially refer to to the process of "initializing" an object's properties in construction, not "setting" them
set
andinit
coexisting in a signature "feels weird"
I also wanted to reiterate my support for the idea of grouping the set
clause as "part of" the base clause, a la Widget() : base(), set(Name) { }
.
- It is syntactically consistent with base clauses on types.
- There are some concerns about the likely ordering requirement where a
set
clause must come after abase
/this
when both are present, and the aesthetics of comma-separating. However, this also makes us more consistent with types in a way, because in a type's base clause, the base type is required to appear before any interface types. This also strengthens the mnemonic of how we decide which properties are required by the caller: first we look at the base/this constructor call, then we take away anything set by this constructor. I'm open to other delimiters betweenbase
/this
andset
, though.
To support the concept of "init all", we could use the syntax init(..)
. This introduces a requirement that a constructor definitely assigns all the "remaining" required properties for the constructor. When used on a method, it would probably mean that the method must set all the required properties on the type--although can be adjusted as we start prototyping the feature.
Using the ..
token evokes the idea of "do it to all of them", a la the expression arr[..]
which produces a slice of an entire collection.
public class Widget
{
public required string Name { get; set; }
public required int Id { get; init; }
public Widget(string name)
: init(..)
{
Name = name;
Id = 42;
}
}
It feels like we could include a "modifier" on the init()
/set()
clause to indicate that it is applied to the return value, not to the this
parameter:
public class WidgetFactory
{
// factory sets some of the widget properties. If the widget has more properties then the user needs to set them.
public Widget CreateWidget()
: return init(Name, Id)
{
return new Widget { Name = "Fred", Id = 42 };
}
// for a factory which doesn't initialize anything, we use an "empty" init clause.
public Widget CreateUninitializedWidget()
: return init()
{
return new Widget { Name = "Fred", Id = 42 };
}
Alternatively, a modifier on the method could indicate the same thing.
// factory sets some of the widget properties. If the widget has more properties then the user needs to set them.
public factory Widget CreateWidget()
: init(Name, Id)
{
return new Widget { Name = "Fred", Id = 42 };
}
// factory doesn't initialize the required properties of 'Widget' on the return value. The user needs to do that.
public factory Widget CreateWidget()
{
return new Widget();
}
// factory sets *all* Widget properties on return value. It feels like this is oxymoronic--just make it a regular method in this case.
public factory Widget CreateWidget()
: init(..)
{
return new Widget { Name = "Fred", Id = 42 };
}
}
I think I personally prefer the return init()
but do not have a strong preference.