Both C++ and C# are missing easy ways to create a new type which is semantically identical to an exisiting type. I find such 'typedefs' totally essential for type-safe programming and its a real shame c# doesn't have them built-in. The difference between void f(string connectionID, string username) to void f(ConID connectionID, UserName username) is obvious ...
(You can achieve something similar in C++ with boost in BOOST_STRONG_TYPEDEF)
It may be tempting to use inheritance but that has some major limitations:
- it will not work for primitive types
- the derived type can still be casted to the original type, ie we can send it to a function receiving our original type, this defeats the whole purpose
- we cannot derive from sealed classes (and ie many .NET classes are sealed)
The only way to achieve a similar thing in C# is by composing our type in a new class:
class SomeType { public void Method() { .. } } sealed class SomeTypeTypeDef { public SomeTypeTypeDef(SomeType composed) { this.Composed = composed; } private SomeType Composed { get; } public override string ToString() => Composed.ToString(); public override int GetHashCode() => HashCode.Combine(Composed); public override bool Equals(object obj) => obj is TDerived o && Composed.Equals(o.Composed); public bool Equals(SomeTypeTypeDefo) => object.Equals(this, o); // proxy the methods we want public void Method() => Composed.Method(); }
While this will work it is very verbose for just a typedef. In addition we have a problem with serializing (ie to Json) as we want to serialize the class through its Composed property.
Below is a helper class that uses the "Curiously Recurring Template Pattern" to make this much simpler:
namespace Typedef { [JsonConverter(typeof(JsonCompositionConverter))] public abstract class Composer<TDerived, T> : IEquatable<TDerived> where TDerived : Composer<TDerived, T> { protected Composer(T composed) { this.Composed = composed; } protected Composer(TDerived d) { this.Composed = d.Composed; } protected T Composed { get; } public override string ToString() => Composed.ToString(); public override int GetHashCode() => HashCode.Combine(Composed); public override bool Equals(object obj) => obj is Composer<TDerived, T> o && Composed.Equals(o.Composed); public bool Equals(TDerived o) => object.Equals(this, o); } class JsonCompositionConverter : JsonConverter { static FieldInfo GetCompositorField(Type t) { var fields = t.BaseType.GetFields(BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.FlattenHierarchy); if (fields.Length!=1) throw new JsonSerializationException(); return fields[0]; } public override bool CanConvert(Type t) { var fields = t.GetFields(BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.FlattenHierarchy); return fields.Length == 1; } // assumes Compositor<T> has either a constructor accepting T or an empty constructor public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) { while (reader.TokenType == JsonToken.Comment && reader.Read()) { }; if (reader.TokenType == JsonToken.Null) return null; var compositorField = GetCompositorField(objectType); var compositorType = compositorField.FieldType; var compositorValue = serializer.Deserialize(reader, compositorType); var ctorT = objectType.GetConstructor(new Type[] { compositorType }); if (!(ctorT is null)) return Activator.CreateInstance(objectType, compositorValue); var ctorEmpty = objectType.GetConstructor(new Type[] { }); if (ctorEmpty is null) throw new JsonSerializationException(); var res = Activator.CreateInstance(objectType); compositorField.SetValue(res, compositorValue); return res; } public override void WriteJson(JsonWriter writer, object o, JsonSerializer serializer) { var compositorField = GetCompositorField(o.GetType()); var value = compositorField.GetValue(o); serializer.Serialize(writer, value); } } }
With Composer the above class becomes simply:
sealed Class SomeTypeTypeDef : Composer<SomeTypeTypeDef, SomeType> { public SomeTypeTypeDef(SomeType composed) : base(composed) {} // proxy the methods we want public void Method() => Composed.Method(); }
And in addition the SomeTypeTypeDef will serialize to Json in the same way that SomeType does.
Hope this helps !