Generating immutable value object in C#: PostSharp or T4 templates?
I'm getting sick of boilerplate immutable value object code. Would either PostSharp or T4 templates allow me to do the following transformation?
Input:
public struct Name
{
public string FirstName;
public string LastName;
}
Output:
public struct Name : IEquatable<Name>
{
private readonly string firstName;
private readonly string lastName;
public string FirstName { get { return this.firstName; } }
public string LastName { get { return this.lastName; } }
public Name(string firstName, string lastName)
{
this.firstName = firstName;
this.lastName = lastName;
}
public bool Equals(Name other)
{
return this.FirstName == other.FirstName && this.LastName == other.LastName;
}
public override bool Equals(object obj)
{
return obj is Name && t开发者_如何学Gohis.Equals((Name)obj);
}
public override int GetHashCode()
{
unchecked
{
int hash = 17;
if (this.FirstName != null)
{
hash = hash * 29 + this.FirstName.GetHashCode();
}
if (this.LastName != null)
{
hash = hash * 29 + this.LastName.GetHashCode();
}
return hash;
}
}
public static bool operator==(Name a, Name b)
{
return a.Equals(b);
}
public static bool operator !=(Name a, Name b)
{
return !(a == b);
}
}
Obviously PostSharp would require a [MakeImmutable]
annotation, which is fine. I'd want the option of generating either a class
or struct
depending on the original type. Also note that GetHashCode
would have to omit the null
checks for value-type members.
I am fairly sure either technology has this capability, but having not been anything but a consumer of PostSharp aspects/T4 templates myself, I don't know which would be a better or easier one for this task. Whichever answer you give, I'll go spend a day or two learning enough to make this work :).
I made something that does what you ask recently (using T4 templates), so it is absolutely possible:
https://github.com/xaviergonz/T4Immutable
For example, given this:
[ImmutableClass(Options = ImmutableClassOptions.IncludeOperatorEquals)]
class Person {
private const int AgeDefaultValue = 18;
public string FirstName { get; }
public string LastName { get; }
public int Age { get; }
[ComputedProperty]
public string FullName {
get {
return FirstName + " " + LastName;
}
}
}
It will automatically generate for you in a separate partial class file the following:
- A constructor such as public Person(string firstName, string lastName, int age = 18) that will initialize the values.
- Working implementations for Equals(object other) and Equals(Person other). Also it will add the IEquatable interface for you. Working implementations for operator== and operator!=
- A working implementation of GetHashCode() A better ToString() with output such as "Person { FirstName=John, LastName=Doe, Age=21 }"
- A Person With(...) method that can be used to generate a new immutable clone with 0 or more properties changed (e.g. var janeDoe = johnDoe.With(firstName: "Jane", age: 20)
So it will generate this (excluding some redundant attributes):
using System;
partial class Person : IEquatable<Person> {
public Person(string firstName, string lastName, int age = 18) {
this.FirstName = firstName;
this.LastName = lastName;
this.Age = age;
_ImmutableHashCode = new { this.FirstName, this.LastName, this.Age }.GetHashCode();
}
private bool ImmutableEquals(Person obj) {
if (ReferenceEquals(this, obj)) return true;
if (ReferenceEquals(obj, null)) return false;
return T4Immutable.Helpers.AreEqual(this.FirstName, obj.FirstName) && T4Immutable.Helpers.AreEqual(this.LastName, obj.LastName) && T4Immutable.Helpers.AreEqual(this.Age, obj.Age);
}
public override bool Equals(object obj) {
return ImmutableEquals(obj as Person);
}
public bool Equals(Person obj) {
return ImmutableEquals(obj);
}
public static bool operator ==(Person a, Person b) {
return T4Immutable.Helpers.AreEqual(a, b);
}
public static bool operator !=(Person a, Person b) {
return !T4Immutable.Helpers.AreEqual(a, b);
}
private readonly int _ImmutableHashCode;
private int ImmutableGetHashCode() {
return _ImmutableHashCode;
}
public override int GetHashCode() {
return ImmutableGetHashCode();
}
private string ImmutableToString() {
var sb = new System.Text.StringBuilder();
sb.Append(nameof(Person) + " { ");
var values = new string[] {
nameof(this.FirstName) + "=" + T4Immutable.Helpers.ToString(this.FirstName),
nameof(this.LastName) + "=" + T4Immutable.Helpers.ToString(this.LastName),
nameof(this.Age) + "=" + T4Immutable.Helpers.ToString(this.Age),
};
sb.Append(string.Join(", ", values) + " }");
return sb.ToString();
}
public override string ToString() {
return ImmutableToString();
}
private Person ImmutableWith(T4Immutable.WithParam<string> firstName = default(T4Immutable.WithParam<string>), T4Immutable.WithParam<string> lastName = default(T4Immutable.WithParam<string>), T4Immutable.WithParam<int> age = default(T4Immutable.WithParam<int>)) {
return new Person(
!firstName.HasValue ? this.FirstName : firstName.Value,
!lastName.HasValue ? this.LastName : lastName.Value,
!age.HasValue ? this.Age : age.Value
);
}
public Person With(T4Immutable.WithParam<string> firstName = default(T4Immutable.WithParam<string>), T4Immutable.WithParam<string> lastName = default(T4Immutable.WithParam<string>), T4Immutable.WithParam<int> age = default(T4Immutable.WithParam<int>)) {
return ImmutableWith(firstName, lastName, age);
}
}
And there are some more features as explained in the project page.
It looks like you want to just decorate your struct with boilerplate stuff, so PostSharp is going to be the way to go. The reason why is because with T4 you would need to
- Find all objects that need to be changed (which still requires attributes or some type of marker)
- Make sure you don't already have the changes applied to the struct
- Use EnvDTE to add the items (not always consistent)
See http://table2dto.codeplex.com for great T4 example of doing this
To do this you would need to employ the use of EnvDTE (see http://dfactor.codeplex.com for code on how to do this)
The problem is, if you need to generate boilerplate code that you need to use at design time (which it doesnt look like you have anything) then it's going to require some thinking to get PostSharp to work for you.
PostSharp is going to be your best bet.
I'd recommend to use ReSharper for this: it generates equality members very well for such cases, so the only thing you must write is a set of property getters.
Also note that normally you must distinguish between structs and objects in Equals
method to check the equality faster, so if this is important, T4 won't be a good choice here.
精彩评论