Records as distinct types
Records are a new construct in C#9, and is a simplified way to create classes which have some intrinsic attributes defined. Value equality, toString, copying, and (simple) immutability.
A record might be defined like this:
public record User(
int Id,
string Nickname,
int Score,
string Name
)
var user = new User(1, "nil", 1337, "Nils");
user.ToString() // Gives us a nice representation of the Users properties
user.Id = 2 // This is not possible, as the value is not modifiable. Changing a value requires the `with` keyword.
var user2 = user with { Id = 2 } // Gives us a copy of the user, but with a new Id
This is nice! But we can further harden the types here. What happens if a Id is added to a Score? They are both stored as int
’, but it makes no sense to add these.
What if we stored the UserId as a UserId
type instead. Its just an int
, but it is no longer comparable with the Score. I believe this concept is named a Distinct type.
Lets define a new class for the Id!
class UserId : IEquatable {
public Id {get;}
UserId(int id){
this.Id = id;
}
// TODO: Implement and maintain GetHashCode
// TODO: Implement and maintain Equals
// TODO: Implement and maintain Equals(T)
// TODO: Implement and maintain ToString
}
❌ This is way too much work. The overhead, and boilerplate code to maintain makes class
unfit for this except in veery specific instances.
Lets try the same with Records in C#9:
record UserId(int Value);
// This automatically implements equality, ToString, HashCode etc for us!
✅ Nice. A Single line for added type-safety, and compile time guarantees!
We would now be much more sure that the Id is used correctly, and not much code is needed.
user.Id + 1 // Error: Cannot add 1 to record UserId (or similar)
user.Id + user2.Id // Error: UserId has no + operator, and cannot be added
user.Id.Value + 1 // This is fine, as it requires explicit intent to access the "internal" value.
user.Score + 1 // A score is just a normal int.
user.Score + user.Id // Error: Cannot add Int and UserId -- Success!
We can now use UserId as params to methods, to avoid mistakes.
public void DeleteUserById(UserId userId){};
DeleteUserById(user.Id); // Works
DeleteUserById(user.Score); // Error: Type int is not compatible with UserId
I do not recommend using this approach for all of your primitive types, but for some it might make sense. If you frequently get bugs from a piece of code this can be a nice approach to catch issues at compile time.
// Nils Henrik