How to implement some constrained .NET struct/structure (let's say LimitedString
), where its state with default values (set by CLR to technical defaults, i.e. nulls, zeros, etc.) should be prohibited due to some design constraint?
For example in case of trivial struct LimitedString
, properties are String Value
and int MaxLength = 10
, where value of the MaxLength
property must be at least 1
. Value 0
is not allowed by design. But when I initalize the structure, I have 0
there. How to force value 10
into defaults?
I see two options:
- Throw exception in
static
(vb.netshared
) parameterless constructor → force using only costructor(s) with parameters. Impractical, parameterless use is expected sometimes. - Add helper private field
IsInitialized
and while it isfalse
, assume default values, i.e.MaxLength = 10
. Slightly higher complexity inside the struct.
Is option #2 a legitimate way or does this violate some design principles? Is there some better way than option #2?
EDIT: option #1 won't work anyway, mentioned constructor is called every time, even if other constructors are called.
-
Let 0 express 10 :) Then make property MaxLength { get { return 10 - maxLength} }; Also add similar transform in ctor. [whoops, I see there is already answer same like my comment]apocalypse– apocalypse2016年05月13日 15:53:45 +00:00Commented May 13, 2016 at 15:53
3 Answers 3
Unless you have a really, really good reason that this has to remain a Struct, I would recommend that you convert it to a Class.
That way, initialisation is completely under your control, through the Constructor(s) of that Class.
If you really can't convert it, then I'd suggest creating a Factory Class to "construct" instances of this Struct; that class then takes on the responsibility for properly initialising instances of the Struct.
-
My actual case is very close to
LimitedString
I have shown. In fact, currently I have a class for it. Having a class for relatively simple value type often included in expressions does not look to me as a good idea, so I attempted to move it to struct. A factory class looks like a good idea (thanks) but programmers would need to be aware of it – maybe not very practical... also when considering its use in expressions.miroxlav– miroxlav2016年05月13日 11:53:21 +00:00Commented May 13, 2016 at 11:53 -
Anyway, good answer. I'll re-evaluate supposed structure usage to decide whether to keep it as
class
or move tostruct
with that defaults trick shown in question as option #2 (code changes were minimal and it works nicely). Thank you for the insight.miroxlav– miroxlav2016年05月13日 12:21:45 +00:00Commented May 13, 2016 at 12:21 -
2Good answer. There have been many objects I have made over the years that fit most of the qualifications for a structure (small, primitive types, value semantics made sense in context), but I chose to change them to immutable classes and then overrode Equals simply because the 0's and null defaults were not a valid state that I wanted to exist. Overriding equals and keeping them immutable gets you close to the same usage as value semantics.Mike– Mike2016年05月13日 15:33:57 +00:00Commented May 13, 2016 at 15:33
-
2How does using factory help? It doesn't prevent the user from creating the default value.svick– svick2016年05月13日 20:44:25 +00:00Commented May 13, 2016 at 20:44
-
I think the biggest problem with accidently generating invalid structs is things like
FirstOrDefault()
Dirk Boer– Dirk Boer2022年11月04日 11:07:03 +00:00Commented Nov 4, 2022 at 11:07
First note: I agree this should probably be a class. For a struct, though:
Is it possible to change the design? In LimitedString
's case, it sounds like 0
is perfectly fine: a string with no characters. You can't enforce anything with a default constructor (like you suggested in #1) because structs (in C#) cannot contain explicit parameterless constructors.
For #2, maybe it's simpler to change the meaning of the struct's state to establish a good by-design default without adding an extra flag or a special-case 0
condition:
private int _maxLengthMinusTen; // By default 0, making MaxLength 10.
public int MaxLength => _maxLengthMinusTen + 10;
A constructor would perform the conversion from an input maxLength
, or you could provide a private setter to keep the 10
localized in the code.
-
private int _maxLengthMinusTen;
That's evil, I like it!svick– svick2016年05月13日 20:43:12 +00:00Commented May 13, 2016 at 20:43 -
good... actually
MaxLength
limit disallowing0
is in databases...char
/varchar
/text
... of course,0
can stay formax
as invarchar(max)
miroxlav– miroxlav2016年05月13日 21:05:37 +00:00Commented May 13, 2016 at 21:05 -
@miroxlav Ah, didn't think about it being dependent on a more set in stone limit. I loosened the language a bit there.31eee384– 31eee3842016年05月13日 21:14:01 +00:00Commented May 13, 2016 at 21:14
How to implement some constrained .NET
struct
, where its state with default values (set by CLR to technical defaults, i.e. nulls, zeros, etc.) should be prohibited due to some design constraint?
You can't. As of 2022 and C# 10.0, there still is no way to prevent consuming code from having default
struct values:
LimitedString[] values = new LimitedString[ 5 ];
MethodThatRequiresNonNullString( values[0].Value ); // <-- This will always fail at runtime, without _any_ compile-time warnings or errors.
Q.E.D.
Whereas if LimitedString
were a class
type, and if C# 8.0 nullable-reference-types were enabled, you'd get a compile-time warning that LimitedString[] values
should be typed as LimitedString?[]
and that the values[0].Value
dereference is unsafe..
However this does not necessarily mean that you should be using a class
type for this: it just means you need to understand how you can implement struct
types correctly and appropriately.
For example in case of trivial
struct LimitedString
, properties areString Value
andint MaxLength
, where value of theMaxLength
property must be at least1
. Value0
is not allowed by design.But when I initialize the structure, I have 0 there. How to force value
10
into defaults?
You appear to be thinking that it's okay to define classes and structs that can be instantiated into an invalid state and then set their properties afterwards until they're somehow "initialized". This is not how classes nor structs should be designed.
(I blame WinForms and WPF/XAML for so many .NET developers getting into this plainly wrong mindset, because WinForms and WPF/XAML basically require all component classes to have parameterless constructors and be post-hoc initialized).
Constructors exist to ensure that their newly created objects are in a specific valid state, this means having to assert preconditions about their parameter values (using ArgumentException
). And in .NET, struct
types should always be immutable (which necessarily means that their properties are strictly get
-only: no set
nor init
properties!) so always write readonly struct
-types, not struct
-types.
With that in mind, let's review your first question again:
But when I initialize the structure, I have 0 there. How to force value
10
into defaults?
You should make maxLength
a constructor parameter, not as a property you set after construction - which means it can also validate the String value
.
However because maxLength
is not an invariant of your program it means your LimitedString
becomes less useful (e.g. e.g. a method that accepts a LimitedString limStr
parameter has no useful compile-time guarantees that the actual limStr.Value.Length
is anything but non-zero, so it would have to check the ``limStr.Value.Lengthitself at runtime which is hardly better than just passing a
Stringvalue. Instead the
MaxLengthvalue should be expressed as a _type-parameter_ of
LimitedString, e.g.
LimitedString<MaxLength: 10>, unfortunately C# does not support
inttype-parameters like C++ does - but you can hack it in other ways... <sub>(but that's another discussion...)</sub> - but I'll continue with my answer anyway, but I'll disregard the _invariance_ aspects of your
MaxLength` design.
Throw exception in
static
parameterless constructor → force using only costructor(s) with parameters. Impractical, parameterless use is expected sometimes.
Again, you misunderstand the purpose of constructors (and are also seemingly getting confused by the type-level static LimitedString()
"static constructor", which is actually completely irrelevant to your question, as your struct LimitedString
won't have any static
members).
You should have a parameterized constructor (accepting String value, Int32 maxLength
) and your constructor must throw new ArgumentException
to make precondition assertions about those parameter's arguments values. That's the whole point of a constructor, regardless of if it's a struct
or a class
's constructor.
...However, because C#/.NET struct
types always have a parameterless constructor that cannot be manually defined or removed (an unavoidable consequence of the low-level details of how struct
types work: it's their default
or "zero" value!) So in C#, whenever you're writing a method with a struct
-type parameter or struct
-typed property-setter you always need to be cognizant of the possibility that that input is default
: and then act accordingly depending on your business/domain rules (i.e. "can a default
or "uninitialized" value of this type ever be considered valid in my program?"). If not, then your program needs to reject it in appropriately: either by throwing an ArgumentException
, returning false
from a Try...
-pattern method, etc.
Add helper private field
IsInitialized
and while it isfalse
, assume default values, i.e.MaxLength = 10
. Slightly higher complexity inside the struct.
You actually wouldn't need to add a whole new field to detect default
struct state: assuming your constructor requires the String value
argument to be non-null
(in addition to checking the length) before storing it in readonly String myStringValue;
then you know that if the struct is default
then the myStringValue
field will also be null
(as null == default(String)
) - so just checking if this.myStringValue is null
is enough to tell you the struct is invalid. But you don't even have to do that: the as you said MaxLength
must be > 0
and because default(Int32) == 0
you could always just check if this.maxLengthValue == default(Int32)
to see if your struct is invalid.
Slightly higher complexity inside the struct.
Unfortunately in the case of struct
types, that "slightly higher complexity" is absolutely necessary because a struct
's member methods and properties can be invoked on default
instances (whereas a class
will never have its instance methods invoked when this == null
), so all of your structs' externally visible (i.e. public
and internal
members) must self-validate this
as a precondition.
Is option #2 a legitimate way or does this violate some design principles?
On the contrary: Option #2 is the only way (and Option #1 is either nonsensical or demonstrates a lack of understanding of OOP fundamentals and the purpose of constructors).
With that in mind, let's review the hard-and-fast rules for struct
type design (especially since C# 7 made significant improvements to struct
types with the addition of readonly struct
, for example):
TL;DR: aka Hard and fast rules:
- If your instance's data is mutable, or exceeds ~32 bytes in aggregate, then then you should use a
class
instead of astruct
. - Use
readonly struct LimitedString
, notstruct LimitedString
. - Define your
struct LimitedString
's state using only fields, not auto-properties...- ...and never read those fields directly!
- Instead all read access to those fields should be indirectly done via wrapper getter-only properties, which all ensure
this != default(LimitedString)
before returning.
- Ensure all members and consumers of your
struct LimitedString
use only those self-validating wrapper properties.
But why?
Because any and all struct
-types can always possibly be default
, it means that if struct-type's default
state is invalid (and so should never be encountered during program operation) it means all public members of structs must be self-validating in some way or another.
I find the best approach is to always use private readonly
fields and require all access to their data to by via an expression-bodied property that performs the state validation. This does mean that you cannot use auto-properties to avoid having to define both the field and property for the same logical member *grumble*.
Rather than using your struct LimitedString
for my first example, let's review this contrived example struct Foo
instead, which features more problem-areas:
public struct Foo
{
public Foo( Bar bar, Qux qux, Int32 neverZero, Int32 canBeZero )
{
if( neverZero == 0 ) throw new ArgumentOutOfRangeException( paramName: nameof(neverZero), actualValue: neverZero, message: "Cannot be zero." );
this.Bar = bar ?? throw new ArgumentNullException(nameof(bar));
this.qux = qux ?? throw new ArgumentNullException(nameof(qux));
this.NeverZero = neverZero;
this.CanBeZero = canBeZero
}
public Bar Bar { get; }
private readonly Qux qux;
public Int32 NeverZero { get; }
public Int32 CanBeZero { get; }
public SomethingElse Baz()
{
return this.Bar.Hmmm( this.qux ).LoremIpsum( 123 );
}
public CompletelyDifferent MyHovercraftIsFullOfEels()
{
return this.qux.IWillNotBuyThistTobaccanistItIsScratched();
}
}
...you'll need this instead:
public readonly struct Foo
{
public Foo( Bar bar, Qux qux, Int32 neverZero, Int32 canBeZero )
{
if( neverZero == 0 ) throw new ArgumentOutOfRangeException( paramName: nameof(neverZero), actualValue: neverZero, message: "Cannot be zero." );
this.bar_DoNotReadDirectlyExceptViaProperty = bar ?? throw new ArgumentNullException(nameof(bar));
this.qux_DoNotReadDirectlyExceptViaProperty = qux ?? throw new ArgumentNullException(nameof(qux));
this.NeverZero = neverZero;
this.CanBeZero = canBeZero
}
private readonly Bar bar_DoNotReadDirectlyExceptViaProperty;
private readonly Qux qux_DoNotReadDirectlyExceptViaProperty;
private readonly Int32 neverZero_DoNotReadDirectlyExceptViaProperty;
private readonly Int32? canBeZero_DoNotReadDirectlyExceptViaProperty;
public Bar Bar => this.bar_DoNotReadDirectlyExceptViaProperty ?? throw new InvalidOperationException();
private Qux Qux => this.qux_DoNotReadDirectlyExceptViaProperty ?? throw new InvalidOperationException();
public Int32 NeverZero => this.neverZero_DoNotReadDirectlyExceptViaProperty != 0 ? this.neverZero_DoNotReadDirectlyExceptViaProperty : throw new InvalidOperationException();
public Int32 CanBeZero => this.canBeZero_DoNotReadDirectlyExceptViaProperty ?? throw new InvalidOperationException();
public SomethingElse Baz()
{
return this.Bar.Hmmm( this.Qux ).LoremIpsum( 123 );
}
public CompletelyDifferent MyHovercraftIsFullOfEels()
{
return this.Qux.IWillNotBuyThistTobaccanistItIsScratched();
}
}
- The main thing to observe here is that now all accesses of
struct Foo
's state (i.e. its fields) from theBaz
andMyHovercraftIsFullOfEels
methods now go via theBar
andQux
properties instead of accessing the fields directly (which would cause aNullReferenceException
, even when using C# 8.0 nullable-reference-types). - The field name style
bar_DoNotReadDirectlyExceptViaProperty
is intentionally ugly: it mitigates the risk of human-factors (e.g. if thestruct Foo
code is ever modified in future by someone who is unfamiliar with the patterns in-use here) and ensures all access go through the self-validating wrapper properties:Bar
andQux
respectively.- Warning sign naming is a common technique, e.g.
dangerouslySetInnerHTML
in ReactJS. I don't know if there's a formal name for the technique though. - Ideally C# would let us have property-scoped fields which would make this technique completely moot but it doesn't. Oh well.
- I suppose you could write a Roslyn code-analysis rule that enforces the requirement that all
readonly struct
fields are always read only via their matched self-validating properties.
- I suppose you could write a Roslyn code-analysis rule that enforces the requirement that all
- Warning sign naming is a common technique, e.g.
- Observe how the self-validating property-getter throws
InvalidOperationException
because that is the specific exception type you should use for this situation (emphasis mine):InvalidOperationException
: The exception that is thrown when a method call is invalid for the object's current state.- I'm sure we're all familiar with the maxim that "property-getters should never throw exceptions", I'll remind everyone that it should really be rephrased as "property-getters on correctly constructed objects should never throw exceptions".
- This is because it's the constructor's responsibility to ensure the object is in a valid state - that's the entire point of constructors in the first place: to make guarantees about the state of an object.
- Therefore, if a
struct
value is not constructed correctly (e.g.SomeStruct val = default(SomeStruct);
) then its property-getters should throw an exception, because it's an exceptional circumstance and you really do what the program to fail.
- I'm sure we're all familiar with the maxim that "property-getters should never throw exceptions", I'll remind everyone that it should really be rephrased as "property-getters on correctly constructed objects should never throw exceptions".
- The
Int32 canBeZero
value is stored in aNullable<Int32>
field (akaInt32?
akaint?
) because otherwise it is impossible to differentiate betweendefault(Int32)
and zero, while usingNullable<Int32>
makes it possible to detect never-initialized fields that storeInt32
values - or any other value-typeT
wheredefault(T)
is meaningful in your application (e.g.TimeSpan
).- However this isn't necessary for the
Int32 NeverZero
property because the domain-rules (which are implemented in the constructor) which specifically disallow zero means thatdefault(Int32)
is an invalid value, so there's no need to useNullable<Int32>
as the underlying field type.
- However this isn't necessary for the
A better struct LimitedString
So having considered that, this is how I would design your struct LimitedString
:
public readonly struct LimitedString
{
public LimitedString( String value, Int32 maxLength )
{
if( value is null ) throw new ArgumentNullException(nameof(value));
if( maxLength < 1 ) throw new ArgumentOutOfRangeException( message: "Value must be positive and non-zero.", nameof(maxLength));
if( value.Length > maxLength ) throw new ArgumentException( message: "Length {0} exceeds maxLength {1}.".Format( value.Length, maxLength ), nameof(value));
this.value_doNotReadDirectly = value;
this.maxLength_doNotReadDirectly = maxLength;
}
private readonly String value_doNotReadDirectly;
private readonly Int32 maxLength_doNotReadDirectly;
public String Value => this.value_doNotReadDirectly ?? throw new InvalidOperationException( "This LimitedString is a default value and was not constructed correctly." );
public Int32 MaxLength => this.maxLength_doNotReadDirectly > 1 ? this.this.maxLength_doNotReadDirectly : throw new InvalidOperationException( "This LimitedString is a default value and was not constructed correctly." );
}
While struct LimitedString
now correctly handles both default
and constructed states, it still needs other improvements made to it in order to make it suitable for production use, such as these listed below:
- With the above
public readonly struct LimitedString
, as-is, Visual Studio will instantly prompt you to overrideEquals
andGetHashCode
.- Implementing
GetHashCode
correctly is very important forstruct
types and is an absolute requirement if you want to use it as aTKey
type in aDictionary<TKey,TValue>
or asT
inHashSet<T>
.
- Implementing
- You'll also want to override
ToString()
too, so you can passLimitedString
directly intoString.Format
args orConsole.Write
, as well as to get a better experience in the VS debugger.- This is trivial: just do
public override String ToString() => this.Value.ToString();
. - Also consider adding
[DebuggerDisplay]
too.
- This is trivial: just do
- Add an
implicit
conversion operator toString
: because every validLimitedString
represents a validString
.- But don't add an
implicit
conversion fromString
: because not every non-nullString
is a validLimitedString
.- And also because
LimitedString
requires amaxLength
parameter, which is not present in a normalString
value, of course.
- And also because
- But don't add an
- Implement
IEquatable<LimitedString>
and maybeIEquatable<String>
too. - Add public methods and forwarded
get
-only properties toLimitedString
that match the public API surface ofString
so that yourLimitedString
type can be a drop-in replacement for aString
parameter or property in your existing code.- so add members like
public Int32 Length => this.Value.Length;
andpublic String Trim() => this.Value.Trim();
. - This can be tedious though, and it is unfortunate that C# still cannot auto-generate those members for us.
- so add members like
A real-world example: Refinement types.
I use readonly struct
-types as refinement types in my projects, which is handy for when I want to express a static constraint over some business object.
For example, supposing I have class User
with an optional String? EmailAddress { get; }
property and I want to pass it around some in code that requires the User
to have a valid e-mail address (not just a non-null String
e-mail address value) then I'd solve this problem by defining readonly struct UserWithValidEmailAddress : IReadOnlyUser
and then changing all member parameters and return-types in the code that requires e-mail addresses from User
to UserWithValidEmailAddress
, which means the compiler will give me a compile-time error if any code tries to pass a User
(because that User
object could have an invalid or absent e-mail address) without first validating it with a validating-factory method.
- I use
readonly struct
because that means it's essentially a zero-cost abstraction, which we don't get withclass
-types (asclass
-types always involve a GC allocation and subsequent collection) whereasstruct
types are "free" until/unless boxed. - The use of
implicit
conversion and implementing theIReadOnly...
interface means thatstruct
-types lack of inheritance aren't problems either (and even if these refinement types wereclass
types instead ofstruct
types inheritance would be the worst way to implement them).- In all of my projects I always define
IReadOnly...
interfaces for all domain-types, soclass User
will haveIReadOnlyUser
which has only read-only versions ofUser
's instance properties in addition to all methods that don't cause any mutation.- Having
IReadOnly...
interfaces is also useful because C# still doesn't have C++-styleconst
-correctness. *grumble*.
- Having
- In all of my projects I always define
Unfortunately because struct
-types can always exist in their default
form, it's necessary for them to self-validate inside every public instance member, not just inside their parameterized constructor. And unfortunately, C# auto-properties (used for brevity) cannot self-validate, which makes them unsafe - so those have to be implemented as private fields with public get-only properties which throw InvalidOperationException
if they detect they're in an invalid/default
state (which is trivial: just ensure that reference-type fields are non-null
, or if all fields are value-types, then use System.Nullable<T>
for the first field and throw if that's null
at runtime - or add a bool
field that will always be true
if any of the defined ctors are used).
So this is what my UserWithValidEmailAddress
looks like:
public readonly struct UserWithValidEmailAddress : IReadOnlyUser
{
public static Boolean TryCreate( User user, [NotNullWhen(true)] out UserWithValidEmailAddress? valid )
{
if( ValidateEmailAddress( user.EmailAddress ) )
{
valid = new ValidateEmailAddress( user, new MailAddress( user.EmailAddress ) );
return true;
}
valid = default;
return false;
}
public static implicit operator User( UserWithValidEmailAddress self )
{
return self.User;
}
private UserWithValidEmailAddress( User user, MailAddress addr )
{
this.user_DoNotReadDirectlyExceptViaProperty = value ?? throw new ArgumentNullException(nameof(user));
this.validatedMailAddress = addr ?? throw new ArgumentNullException(nameof(value));
}
private readonly User user_DoNotReadDirectlyExceptViaProperty;
private readonly MailAddress validatedMailAddress;
public User User => this.user_DoNotReadDirectlyExceptViaProperty ?? throw new InvalidOperationException("This is a default(UserWithValidEmailAddress).");
public MailAddress ValidEmailAddress => this.validatedMailAddress ?? throw new InvalidOperationException("This is a default(UserWithValidEmailAddress).");
public override String ToString() => this.User.ToString();
#region IReadOnlyUser
// This code-block is generated by my own tooling, which helps with the tedium.
// Because all of these members go through `UserWithValidEmailAddress.User` (which self-validates) instead of directly accessing the `user_DoNotReadDirectlyExceptViaProperty` field it means `default`-safety is guaranteed.
public String UserName => this.User.UserName;
public String DisplayName => this.User.DisplayName;
public DateTime Created => this.User.Created;
// etc
// The IReadOnlyUser.EmailAddress property is implemented explicitly, instead of as a public member, because it's redundant: if a method is using `UserWithValidEmailAddress` then it should use the `MailAddress ValidEmailAddress` property instead.
String IReadOnlyUser.EmailAddress => this.User.EmailAddress;
#endregion
}
While this looks tedious to write every time I want a refinement type, I have a VS code-snippet that inserts the outline for me, as well as having other tooling to automatically generate the #region IReadOnlyUser
part too.
Explore related questions
See similar questions with these tags.