Skip to content

Instantly share code, notes, and snippets.

@CaCTuCaTu4ECKuu
Created October 5, 2020 03:19
Show Gist options
  • Save CaCTuCaTu4ECKuu/801a7a6297a8db54ad099ac75f0e20ab to your computer and use it in GitHub Desktop.
Save CaCTuCaTu4ECKuu/801a7a6297a8db54ad099ac75f0e20ab to your computer and use it in GitHub Desktop.
Static properties initialization does not work when inheriting
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Reflection;
namespace TypeSafeEnum
{
[DebuggerDisplay("{Name}")]
public abstract class StringEnum<T> where T : StringEnum<T>, IEquatable<T>
{
protected static Dictionary<string, T> _possibleValues;
protected string[] _values;
public string Name
{
get => string.Join(",", _values);
private set => _values = new[] { value };
}
static StringEnum()
{
_possibleValues = new Dictionary<string, T>(StringComparer.OrdinalIgnoreCase);
}
protected StringEnum(string name)
{
Name = name;
}
protected StringEnum(string[] names)
{
_values = names;
}
private static readonly BindingFlags _createBindingFlags = BindingFlags.NonPublic | BindingFlags.Instance;
private static T CreateInstance(object[] val)
{
return (T)Activator.CreateInstance(typeof(T), _createBindingFlags, null, val, null);
}
public bool HasFlag(T value)
{
if (value._values.Length == 1)
return _values.Contains(value.Name);
else if (value._values.Length == _values.Length)
{
foreach (var val in value._values)
{
if (!_values.Contains(val))
return false;
}
return true;
}
return false;
}
protected static T RegisterPossibleValue(string name)
{
if (_possibleValues.ContainsKey(name))
throw new ArgumentException();
var value = CreateInstance(new object[] { name });
_possibleValues.Add(name, value);
return value;
}
public static IEnumerable<T> GetAll() => _possibleValues.Values;
public static bool IsValidValue(string key) => _possibleValues.ContainsKey(key);
public static T Parse(string key)
{
if (key.IndexOf(',') < 0)
{
if (_possibleValues.ContainsKey(key))
return _possibleValues[key];
}
else
{
string[] values = key.Split(',');
if (values.Length > 0)
{
List<string> names = new List<string>();
for (int i = 0; i < values.Count(); i++)
{
if (_possibleValues.ContainsKey(values[i]))
names.Add(_possibleValues[values[i]].Name);
else
break;
}
if (names.Distinct().Count() == values.Length)
return CreateInstance(new object[] { names.ToArray() });
}
}
throw new ArgumentException();
}
public static bool TryParse(string key, out T result)
{
if (key.IndexOf(',') < 0)
{
if (_possibleValues.ContainsKey(key))
{
result = _possibleValues[key];
return true;
}
}
else
{
string[] values = key.Split(',');
if (values.Length > 0)
{
List<string> names = new List<string>();
for (int i = 0; i < values.Count(); i++)
{
if (_possibleValues.ContainsKey(values[i]))
names.Add(_possibleValues[values[i]].Name);
}
if (names.Any())
{
var dNames = names.Distinct();
if (dNames.Count() > 1)
result = CreateInstance(new object[] { dNames.ToArray() });
else
result = _possibleValues[dNames.ElementAt(0)];
return true;
}
}
}
result = null;
return false;
}
public override string ToString() => Name;
public bool Equals(T other)
{
return this == other;
}
public override bool Equals(object obj)
{
return Equals(obj as T);
}
public override int GetHashCode()
{
var hashCode = 3487623;
foreach (var val in _values)
hashCode ^= val.GetHashCode();
return hashCode;
}
public static bool operator ==(StringEnum<T> left, StringEnum<T> right)
{
if (ReferenceEquals(left, right))
return true;
if (ReferenceEquals(right, null) || ReferenceEquals(left, null))
return false;
return left._values.Length == right._values.Length
&& left._values.All(e => right._values.Any(x => string.Equals(e, x, StringComparison.OrdinalIgnoreCase)));
}
public static bool operator !=(StringEnum<T> left, StringEnum<T> right)
{
return !(left == right);
}
public static implicit operator string(StringEnum<T> value) => value.Name;
public static StringEnum<T> operator |(StringEnum<T> a, StringEnum<T> b)
{
if (a == b)
return a;
var dNames = a._values.Concat(b._values).Distinct().ToArray();
return CreateInstance(new object[] { dNames });
}
}
}
using System;
using System.Linq;
using Xunit;
namespace TypeSafeEnum.Tests
{
using Models;
public class StringEnumTest
{
[Fact]
public void TestCount()
{
Assert.Equal(3, StringEnumTestModel.GetAll().Count());
}
[Fact]
public void TestEquals()
{
var a = StringEnumTestModel.A;
Assert.Equal(StringEnumTestModel.A, a);
Assert.NotEqual(StringEnumTestModel.B, a);
}
[Fact]
public void TestEqualityComparer()
{
var b = StringEnumTestModel.B;
Assert.True(b == StringEnumTestModel.B);
Assert.False(b != StringEnumTestModel.B);
}
[Fact]
public void TestNullEqualityComparer()
{
Assert.False(StringEnumTestModel.A == null);
Assert.False(null == StringEnumTestModel.A);
Assert.True(StringEnumTestModel.A != null);
Assert.True(null != StringEnumTestModel.A);
StringEnumTestModel e = null;
Assert.True(e == null);
Assert.False(e == StringEnumTestModel.C);
}
[Fact]
public void TestFlags()
{
var x = StringEnumTestModel.A | StringEnumTestModel.B;
Assert.NotEqual(StringEnumTestModel.A, x);
Assert.NotEqual(StringEnumTestModel.B, x);
Assert.True(x.HasFlag(StringEnumTestModel.A));
Assert.True(x.HasFlag(StringEnumTestModel.B));
Assert.False(x.HasFlag(StringEnumTestModel.C));
}
[Fact]
public void TestFlagsEqualityComparer()
{
var x1 = StringEnumTestModel.A | StringEnumTestModel.B;
var x2 = StringEnumTestModel.B | StringEnumTestModel.A;
var x3 = StringEnumTestModel.A | StringEnumTestModel.C;
var x4 = StringEnumTestModel.A | StringEnumTestModel.B | StringEnumTestModel.C;
Assert.Equal(x1, x2);
Assert.Equal(x2, x1);
Assert.NotEqual(x1, x3);
Assert.NotEqual(x3, x2);
Assert.NotEqual(x2, x4);
Assert.NotEqual(x4, x2);
Assert.NotEqual(x3, x4);
Assert.NotEqual(x4, x3);
}
[Fact]
public void TestHashCode()
{
var x1 = StringEnumTestModel.A | StringEnumTestModel.B;
var x2 = StringEnumTestModel.B | StringEnumTestModel.A;
Assert.Equal(x1.GetHashCode(), x2.GetHashCode());
Assert.NotEqual(StringEnumTestModel.A.GetHashCode(), x1.GetHashCode());
Assert.NotEqual(StringEnumTestModel.B.GetHashCode(), x1.GetHashCode());
Assert.NotEqual(StringEnumTestModel.A.GetHashCode(), x2.GetHashCode());
Assert.NotEqual(StringEnumTestModel.B.GetHashCode(), x2.GetHashCode());
}
[Fact]
public void TestValidValueCheck()
{
Assert.True(StringEnumTestModel.IsValidValue("A"));
Assert.True(StringEnumTestModel.IsValidValue("a"));
Assert.False(StringEnumTestModel.IsValidValue("x"));
Assert.False(StringEnumTestModel.IsValidValue("4"));
Assert.False(StringEnumTestModel.IsValidValue("AAaaa"));
Assert.False(StringEnumTestModel.IsValidValue("aaa"));
Assert.False(StringEnumTestModel.IsValidValue("a "));
Assert.False(StringEnumTestModel.IsValidValue("A "));
Assert.False(StringEnumTestModel.IsValidValue(" a"));
Assert.False(StringEnumTestModel.IsValidValue(" A"));
}
}
}
using System;
namespace TypeSafeEnum.Models
{
public sealed class StringEnumTestModel : StringEnum<StringEnumTestModel>, IEquatable<StringEnumTestModel>
{
public static StringEnumTestModel A = RegisterPossibleValue("a");
public static StringEnumTestModel B = RegisterPossibleValue("b");
public static StringEnumTestModel C = RegisterPossibleValue("c");
private StringEnumTestModel(string name) : base(name)
{ }
private StringEnumTestModel(string[] names) : base(names)
{ }
}
}
@CaCTuCaTu4ECKuu
Copy link
Author

Sorry for extra code, I decided it's better to send as is

@KalleOlaviNiemitalo
Copy link

I think you need OrdinalIgnoreCase in GetHashCode as well. (Not related to dotnet/runtime#43022.)

@CaCTuCaTu4ECKuu
Copy link
Author

There's no way to get instance with value which is not registred with any casual method so I believe there is no reason. Or maybe I just dont see it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment