General purpose replacement for enum with FlagsAttribute
.everyoneloves__top-leaderboard:empty,.everyoneloves__mid-leaderboard:empty,.everyoneloves__bot-mid-leaderboard:empty{
margin-bottom:0;
}
$begingroup$
Enums with the FlagsAttribute
have the disadvantage that you need to be careful when assigning their values. They are also inconvenient when you would like to allow the user of the library to add their own options. The enum
is closed/final.
My alternative Option
class should solve these two issues. It can be used either on its own or be inherited from. The two static Create
factories take care of the Flag
value for the specified Category
. The Category
groups options together. HasFlag
is called here Contains
. Other than this it also implements the usual set of operators and parsing.
[PublicAPI]
[DebuggerDisplay(DebuggerDisplayString.DefaultNoQuotes)]
public class Option : IEquatable<Option>, IComparable<Option>, IComparable
{
private static readonly OptionComparer Comparer = new OptionComparer();
private static readonly ConcurrentDictionary<SoftString, int> Flags = new ConcurrentDictionary<SoftString, int>();
public Option(SoftString category, SoftString name, int flag)
{
Category = category;
Name = name;
Flag = flag;
}
private string DebuggerDisplay => ToString();
[AutoEqualityProperty]
public SoftString Category { [DebuggerStepThrough] get; }
public SoftString Name { [DebuggerStepThrough] get; }
[AutoEqualityProperty]
public int Flag { [DebuggerStepThrough] get; }
public static Option Create(string category, string name)
{
return new Option(category, name, NextFlag(category));
}
[NotNull]
public static T Create<T>(string name) where T : Option
{
return (T)Activator.CreateInstance(typeof(T), name, NextFlag(typeof(T).Name));
}
private static int NextFlag(string category)
{
return Flags.AddOrUpdate(category, t => 0, (k, flag) => flag == 0 ? 1 : flag << 1);
}
public static Option Parse([NotNull] string value, params Option options)
{
if (value == null) throw new ArgumentNullException(nameof(value));
if (options.Select(o => o.Category).Distinct().Count() > 1) throw new ArgumentException("All options must have the same category.");
return options.FirstOrDefault(o => o.Name == value) ?? throw DynamicException.Create("OptionOutOfRange", $"There is no such option as '{value}'.");
}
public static Option FromValue(int value, params Option options)
{
if (options.Select(o => o.Category).Distinct().Count() > 1) throw new ArgumentException("All options must have the same category.");
return
options
.Where(o => (o.Flag & value) == o.Flag)
.Aggregate((current, next) => new Option(options.First().Category, "Custom", current.Flag | next.Flag));
}
public bool Contains(params Option options) => Contains(options.Aggregate((current, next) => current.Flag | next.Flag).Flag);
public bool Contains(int flags) => (Flag & flags) == flags;
[DebuggerStepThrough]
public override string ToString() => $"{Category.ToString()}.{Name.ToString()}";
#region IEquatable
public bool Equals(Option other) => AutoEquality<Option>.Comparer.Equals(this, other);
public override bool Equals(object obj) => Equals(obj as Option);
public override int GetHashCode() => AutoEquality<Option>.Comparer.GetHashCode(this);
#endregion
public int CompareTo(Option other) => Comparer.Compare(this, other);
public int CompareTo(object other) => Comparer.Compare(this, other);
public static implicit operator string(Option option) => option?.ToString() ?? throw new ArgumentNullException(nameof(option));
public static implicit operator int(Option option) => option?.Flag ?? throw new ArgumentNullException(nameof(option));
public static implicit operator Option(string value) => Parse(value);
public static implicit operator Option(int value) => FromValue(value);
#region Operators
public static bool operator ==(Option left, Option right) => Comparer.Compare(left, right) == 0;
public static bool operator !=(Option left, Option right) => !(left == right);
public static bool operator <(Option left, Option right) => Comparer.Compare(left, right) < 0;
public static bool operator <=(Option left, Option right) => Comparer.Compare(left, right) <= 0;
public static bool operator >(Option left, Option right) => Comparer.Compare(left, right) > 0;
public static bool operator >=(Option left, Option right) => Comparer.Compare(left, right) >= 0;
public static Option operator |(Option left, Option right) => new Option(left.Category, "Custom", left.Flag | right.Flag);
#endregion
private class OptionComparer : IComparer<Option>, IComparer
{
public int Compare(Option left, Option right)
{
if (ReferenceEquals(left, right)) return 0;
if (ReferenceEquals(left, null)) return 1;
if (ReferenceEquals(right, null)) return -1;
return left.Flag - right.Flag;
}
public int Compare(object left, object right) => Compare(left as Option, right as Option);
}
}
This should replace the previous enum
[Flags]
public enum FeatureOptions
{
None = 0,
/// <summary>
/// When set a feature is enabled.
/// </summary>
Enabled = 1 << 0,
/// <summary>
/// When set a warning is logged when a feature is toggled.
/// </summary>
Warn = 1 << 1,
/// <summary>
/// When set feature usage statistics are logged.
/// </summary>
Telemetry = 1 << 2, // For future use
}
with
[PublicAPI]
public static class FeatureOptionsNew
{
public static readonly FeatureOption None = Option.Create<FeatureOption>(nameof(None));
/// <summary>
/// When set a feature is enabled.
/// </summary>
public static readonly FeatureOption Enable = Option.Create<FeatureOption>(nameof(Enable));
/// <summary>
/// When set a warning is logged when a feature is toggled.
/// </summary>
public static readonly FeatureOption Warn = Option.Create<FeatureOption>(nameof(Warn));
/// <summary>
/// When set feature usage statistics are logged.
/// </summary>
public static readonly FeatureOption Telemetry = Option.Create<FeatureOption>(nameof(Warn));
}
that is based on a new FeatureOption
type
public class FeatureOption : Option
{
public FeatureOption(string name, int value) : base(nameof(FeatureOption), name, value) { }
}
They can be used exactly like classic enum
s:
public class OptionTest
{
[Fact]
public void Examples()
{
Assert.Equal(new { 0, 1, 2, 4 }, new
{
FeatureOptionsNew.None,
FeatureOptionsNew.Enable,
FeatureOptionsNew.Warn,
FeatureOptionsNew.Telemetry
}.Select(o => o.Flag));
Assert.Equal(FeatureOptionsNew.Enable, FeatureOptionsNew.Enable);
Assert.NotEqual(FeatureOptionsNew.Enable, FeatureOptionsNew.Telemetry);
var oParsed = Option.Parse("Warn", FeatureOptionsNew.Enable, FeatureOptionsNew.Warn, FeatureOptionsNew.Telemetry);
Assert.Equal(FeatureOptionsNew.Warn, oParsed);
var oFromValue = Option.FromValue(3, FeatureOptionsNew.Enable, FeatureOptionsNew.Warn, FeatureOptionsNew.Telemetry);
Assert.Equal(FeatureOptionsNew.Enable | FeatureOptionsNew.Warn, oFromValue);
Assert.True(FeatureOptionsNew.None < FeatureOptionsNew.Enable);
Assert.True(FeatureOptionsNew.Enable < FeatureOptionsNew.Telemetry);
}
}
Questions
- Is this as extendable as I think it is?
- Are there any APIs missing that I didn't think of or would be convenient?
- What do you think about the automatic
Flag
maintenance and options creation?
c# api inheritance enum
$endgroup$
add a comment
|
$begingroup$
Enums with the FlagsAttribute
have the disadvantage that you need to be careful when assigning their values. They are also inconvenient when you would like to allow the user of the library to add their own options. The enum
is closed/final.
My alternative Option
class should solve these two issues. It can be used either on its own or be inherited from. The two static Create
factories take care of the Flag
value for the specified Category
. The Category
groups options together. HasFlag
is called here Contains
. Other than this it also implements the usual set of operators and parsing.
[PublicAPI]
[DebuggerDisplay(DebuggerDisplayString.DefaultNoQuotes)]
public class Option : IEquatable<Option>, IComparable<Option>, IComparable
{
private static readonly OptionComparer Comparer = new OptionComparer();
private static readonly ConcurrentDictionary<SoftString, int> Flags = new ConcurrentDictionary<SoftString, int>();
public Option(SoftString category, SoftString name, int flag)
{
Category = category;
Name = name;
Flag = flag;
}
private string DebuggerDisplay => ToString();
[AutoEqualityProperty]
public SoftString Category { [DebuggerStepThrough] get; }
public SoftString Name { [DebuggerStepThrough] get; }
[AutoEqualityProperty]
public int Flag { [DebuggerStepThrough] get; }
public static Option Create(string category, string name)
{
return new Option(category, name, NextFlag(category));
}
[NotNull]
public static T Create<T>(string name) where T : Option
{
return (T)Activator.CreateInstance(typeof(T), name, NextFlag(typeof(T).Name));
}
private static int NextFlag(string category)
{
return Flags.AddOrUpdate(category, t => 0, (k, flag) => flag == 0 ? 1 : flag << 1);
}
public static Option Parse([NotNull] string value, params Option options)
{
if (value == null) throw new ArgumentNullException(nameof(value));
if (options.Select(o => o.Category).Distinct().Count() > 1) throw new ArgumentException("All options must have the same category.");
return options.FirstOrDefault(o => o.Name == value) ?? throw DynamicException.Create("OptionOutOfRange", $"There is no such option as '{value}'.");
}
public static Option FromValue(int value, params Option options)
{
if (options.Select(o => o.Category).Distinct().Count() > 1) throw new ArgumentException("All options must have the same category.");
return
options
.Where(o => (o.Flag & value) == o.Flag)
.Aggregate((current, next) => new Option(options.First().Category, "Custom", current.Flag | next.Flag));
}
public bool Contains(params Option options) => Contains(options.Aggregate((current, next) => current.Flag | next.Flag).Flag);
public bool Contains(int flags) => (Flag & flags) == flags;
[DebuggerStepThrough]
public override string ToString() => $"{Category.ToString()}.{Name.ToString()}";
#region IEquatable
public bool Equals(Option other) => AutoEquality<Option>.Comparer.Equals(this, other);
public override bool Equals(object obj) => Equals(obj as Option);
public override int GetHashCode() => AutoEquality<Option>.Comparer.GetHashCode(this);
#endregion
public int CompareTo(Option other) => Comparer.Compare(this, other);
public int CompareTo(object other) => Comparer.Compare(this, other);
public static implicit operator string(Option option) => option?.ToString() ?? throw new ArgumentNullException(nameof(option));
public static implicit operator int(Option option) => option?.Flag ?? throw new ArgumentNullException(nameof(option));
public static implicit operator Option(string value) => Parse(value);
public static implicit operator Option(int value) => FromValue(value);
#region Operators
public static bool operator ==(Option left, Option right) => Comparer.Compare(left, right) == 0;
public static bool operator !=(Option left, Option right) => !(left == right);
public static bool operator <(Option left, Option right) => Comparer.Compare(left, right) < 0;
public static bool operator <=(Option left, Option right) => Comparer.Compare(left, right) <= 0;
public static bool operator >(Option left, Option right) => Comparer.Compare(left, right) > 0;
public static bool operator >=(Option left, Option right) => Comparer.Compare(left, right) >= 0;
public static Option operator |(Option left, Option right) => new Option(left.Category, "Custom", left.Flag | right.Flag);
#endregion
private class OptionComparer : IComparer<Option>, IComparer
{
public int Compare(Option left, Option right)
{
if (ReferenceEquals(left, right)) return 0;
if (ReferenceEquals(left, null)) return 1;
if (ReferenceEquals(right, null)) return -1;
return left.Flag - right.Flag;
}
public int Compare(object left, object right) => Compare(left as Option, right as Option);
}
}
This should replace the previous enum
[Flags]
public enum FeatureOptions
{
None = 0,
/// <summary>
/// When set a feature is enabled.
/// </summary>
Enabled = 1 << 0,
/// <summary>
/// When set a warning is logged when a feature is toggled.
/// </summary>
Warn = 1 << 1,
/// <summary>
/// When set feature usage statistics are logged.
/// </summary>
Telemetry = 1 << 2, // For future use
}
with
[PublicAPI]
public static class FeatureOptionsNew
{
public static readonly FeatureOption None = Option.Create<FeatureOption>(nameof(None));
/// <summary>
/// When set a feature is enabled.
/// </summary>
public static readonly FeatureOption Enable = Option.Create<FeatureOption>(nameof(Enable));
/// <summary>
/// When set a warning is logged when a feature is toggled.
/// </summary>
public static readonly FeatureOption Warn = Option.Create<FeatureOption>(nameof(Warn));
/// <summary>
/// When set feature usage statistics are logged.
/// </summary>
public static readonly FeatureOption Telemetry = Option.Create<FeatureOption>(nameof(Warn));
}
that is based on a new FeatureOption
type
public class FeatureOption : Option
{
public FeatureOption(string name, int value) : base(nameof(FeatureOption), name, value) { }
}
They can be used exactly like classic enum
s:
public class OptionTest
{
[Fact]
public void Examples()
{
Assert.Equal(new { 0, 1, 2, 4 }, new
{
FeatureOptionsNew.None,
FeatureOptionsNew.Enable,
FeatureOptionsNew.Warn,
FeatureOptionsNew.Telemetry
}.Select(o => o.Flag));
Assert.Equal(FeatureOptionsNew.Enable, FeatureOptionsNew.Enable);
Assert.NotEqual(FeatureOptionsNew.Enable, FeatureOptionsNew.Telemetry);
var oParsed = Option.Parse("Warn", FeatureOptionsNew.Enable, FeatureOptionsNew.Warn, FeatureOptionsNew.Telemetry);
Assert.Equal(FeatureOptionsNew.Warn, oParsed);
var oFromValue = Option.FromValue(3, FeatureOptionsNew.Enable, FeatureOptionsNew.Warn, FeatureOptionsNew.Telemetry);
Assert.Equal(FeatureOptionsNew.Enable | FeatureOptionsNew.Warn, oFromValue);
Assert.True(FeatureOptionsNew.None < FeatureOptionsNew.Enable);
Assert.True(FeatureOptionsNew.Enable < FeatureOptionsNew.Telemetry);
}
}
Questions
- Is this as extendable as I think it is?
- Are there any APIs missing that I didn't think of or would be convenient?
- What do you think about the automatic
Flag
maintenance and options creation?
c# api inheritance enum
$endgroup$
$begingroup$
The current commit.
$endgroup$
– t3chb0t
May 26 at 12:06
$begingroup$
Comments are not for extended discussion; this conversation has been moved to chat.
$endgroup$
– rolfl♦
May 27 at 13:15
$begingroup$
wow, I got another downvote... how so?
$endgroup$
– t3chb0t
Jul 12 at 11:50
add a comment
|
$begingroup$
Enums with the FlagsAttribute
have the disadvantage that you need to be careful when assigning their values. They are also inconvenient when you would like to allow the user of the library to add their own options. The enum
is closed/final.
My alternative Option
class should solve these two issues. It can be used either on its own or be inherited from. The two static Create
factories take care of the Flag
value for the specified Category
. The Category
groups options together. HasFlag
is called here Contains
. Other than this it also implements the usual set of operators and parsing.
[PublicAPI]
[DebuggerDisplay(DebuggerDisplayString.DefaultNoQuotes)]
public class Option : IEquatable<Option>, IComparable<Option>, IComparable
{
private static readonly OptionComparer Comparer = new OptionComparer();
private static readonly ConcurrentDictionary<SoftString, int> Flags = new ConcurrentDictionary<SoftString, int>();
public Option(SoftString category, SoftString name, int flag)
{
Category = category;
Name = name;
Flag = flag;
}
private string DebuggerDisplay => ToString();
[AutoEqualityProperty]
public SoftString Category { [DebuggerStepThrough] get; }
public SoftString Name { [DebuggerStepThrough] get; }
[AutoEqualityProperty]
public int Flag { [DebuggerStepThrough] get; }
public static Option Create(string category, string name)
{
return new Option(category, name, NextFlag(category));
}
[NotNull]
public static T Create<T>(string name) where T : Option
{
return (T)Activator.CreateInstance(typeof(T), name, NextFlag(typeof(T).Name));
}
private static int NextFlag(string category)
{
return Flags.AddOrUpdate(category, t => 0, (k, flag) => flag == 0 ? 1 : flag << 1);
}
public static Option Parse([NotNull] string value, params Option options)
{
if (value == null) throw new ArgumentNullException(nameof(value));
if (options.Select(o => o.Category).Distinct().Count() > 1) throw new ArgumentException("All options must have the same category.");
return options.FirstOrDefault(o => o.Name == value) ?? throw DynamicException.Create("OptionOutOfRange", $"There is no such option as '{value}'.");
}
public static Option FromValue(int value, params Option options)
{
if (options.Select(o => o.Category).Distinct().Count() > 1) throw new ArgumentException("All options must have the same category.");
return
options
.Where(o => (o.Flag & value) == o.Flag)
.Aggregate((current, next) => new Option(options.First().Category, "Custom", current.Flag | next.Flag));
}
public bool Contains(params Option options) => Contains(options.Aggregate((current, next) => current.Flag | next.Flag).Flag);
public bool Contains(int flags) => (Flag & flags) == flags;
[DebuggerStepThrough]
public override string ToString() => $"{Category.ToString()}.{Name.ToString()}";
#region IEquatable
public bool Equals(Option other) => AutoEquality<Option>.Comparer.Equals(this, other);
public override bool Equals(object obj) => Equals(obj as Option);
public override int GetHashCode() => AutoEquality<Option>.Comparer.GetHashCode(this);
#endregion
public int CompareTo(Option other) => Comparer.Compare(this, other);
public int CompareTo(object other) => Comparer.Compare(this, other);
public static implicit operator string(Option option) => option?.ToString() ?? throw new ArgumentNullException(nameof(option));
public static implicit operator int(Option option) => option?.Flag ?? throw new ArgumentNullException(nameof(option));
public static implicit operator Option(string value) => Parse(value);
public static implicit operator Option(int value) => FromValue(value);
#region Operators
public static bool operator ==(Option left, Option right) => Comparer.Compare(left, right) == 0;
public static bool operator !=(Option left, Option right) => !(left == right);
public static bool operator <(Option left, Option right) => Comparer.Compare(left, right) < 0;
public static bool operator <=(Option left, Option right) => Comparer.Compare(left, right) <= 0;
public static bool operator >(Option left, Option right) => Comparer.Compare(left, right) > 0;
public static bool operator >=(Option left, Option right) => Comparer.Compare(left, right) >= 0;
public static Option operator |(Option left, Option right) => new Option(left.Category, "Custom", left.Flag | right.Flag);
#endregion
private class OptionComparer : IComparer<Option>, IComparer
{
public int Compare(Option left, Option right)
{
if (ReferenceEquals(left, right)) return 0;
if (ReferenceEquals(left, null)) return 1;
if (ReferenceEquals(right, null)) return -1;
return left.Flag - right.Flag;
}
public int Compare(object left, object right) => Compare(left as Option, right as Option);
}
}
This should replace the previous enum
[Flags]
public enum FeatureOptions
{
None = 0,
/// <summary>
/// When set a feature is enabled.
/// </summary>
Enabled = 1 << 0,
/// <summary>
/// When set a warning is logged when a feature is toggled.
/// </summary>
Warn = 1 << 1,
/// <summary>
/// When set feature usage statistics are logged.
/// </summary>
Telemetry = 1 << 2, // For future use
}
with
[PublicAPI]
public static class FeatureOptionsNew
{
public static readonly FeatureOption None = Option.Create<FeatureOption>(nameof(None));
/// <summary>
/// When set a feature is enabled.
/// </summary>
public static readonly FeatureOption Enable = Option.Create<FeatureOption>(nameof(Enable));
/// <summary>
/// When set a warning is logged when a feature is toggled.
/// </summary>
public static readonly FeatureOption Warn = Option.Create<FeatureOption>(nameof(Warn));
/// <summary>
/// When set feature usage statistics are logged.
/// </summary>
public static readonly FeatureOption Telemetry = Option.Create<FeatureOption>(nameof(Warn));
}
that is based on a new FeatureOption
type
public class FeatureOption : Option
{
public FeatureOption(string name, int value) : base(nameof(FeatureOption), name, value) { }
}
They can be used exactly like classic enum
s:
public class OptionTest
{
[Fact]
public void Examples()
{
Assert.Equal(new { 0, 1, 2, 4 }, new
{
FeatureOptionsNew.None,
FeatureOptionsNew.Enable,
FeatureOptionsNew.Warn,
FeatureOptionsNew.Telemetry
}.Select(o => o.Flag));
Assert.Equal(FeatureOptionsNew.Enable, FeatureOptionsNew.Enable);
Assert.NotEqual(FeatureOptionsNew.Enable, FeatureOptionsNew.Telemetry);
var oParsed = Option.Parse("Warn", FeatureOptionsNew.Enable, FeatureOptionsNew.Warn, FeatureOptionsNew.Telemetry);
Assert.Equal(FeatureOptionsNew.Warn, oParsed);
var oFromValue = Option.FromValue(3, FeatureOptionsNew.Enable, FeatureOptionsNew.Warn, FeatureOptionsNew.Telemetry);
Assert.Equal(FeatureOptionsNew.Enable | FeatureOptionsNew.Warn, oFromValue);
Assert.True(FeatureOptionsNew.None < FeatureOptionsNew.Enable);
Assert.True(FeatureOptionsNew.Enable < FeatureOptionsNew.Telemetry);
}
}
Questions
- Is this as extendable as I think it is?
- Are there any APIs missing that I didn't think of or would be convenient?
- What do you think about the automatic
Flag
maintenance and options creation?
c# api inheritance enum
$endgroup$
Enums with the FlagsAttribute
have the disadvantage that you need to be careful when assigning their values. They are also inconvenient when you would like to allow the user of the library to add their own options. The enum
is closed/final.
My alternative Option
class should solve these two issues. It can be used either on its own or be inherited from. The two static Create
factories take care of the Flag
value for the specified Category
. The Category
groups options together. HasFlag
is called here Contains
. Other than this it also implements the usual set of operators and parsing.
[PublicAPI]
[DebuggerDisplay(DebuggerDisplayString.DefaultNoQuotes)]
public class Option : IEquatable<Option>, IComparable<Option>, IComparable
{
private static readonly OptionComparer Comparer = new OptionComparer();
private static readonly ConcurrentDictionary<SoftString, int> Flags = new ConcurrentDictionary<SoftString, int>();
public Option(SoftString category, SoftString name, int flag)
{
Category = category;
Name = name;
Flag = flag;
}
private string DebuggerDisplay => ToString();
[AutoEqualityProperty]
public SoftString Category { [DebuggerStepThrough] get; }
public SoftString Name { [DebuggerStepThrough] get; }
[AutoEqualityProperty]
public int Flag { [DebuggerStepThrough] get; }
public static Option Create(string category, string name)
{
return new Option(category, name, NextFlag(category));
}
[NotNull]
public static T Create<T>(string name) where T : Option
{
return (T)Activator.CreateInstance(typeof(T), name, NextFlag(typeof(T).Name));
}
private static int NextFlag(string category)
{
return Flags.AddOrUpdate(category, t => 0, (k, flag) => flag == 0 ? 1 : flag << 1);
}
public static Option Parse([NotNull] string value, params Option options)
{
if (value == null) throw new ArgumentNullException(nameof(value));
if (options.Select(o => o.Category).Distinct().Count() > 1) throw new ArgumentException("All options must have the same category.");
return options.FirstOrDefault(o => o.Name == value) ?? throw DynamicException.Create("OptionOutOfRange", $"There is no such option as '{value}'.");
}
public static Option FromValue(int value, params Option options)
{
if (options.Select(o => o.Category).Distinct().Count() > 1) throw new ArgumentException("All options must have the same category.");
return
options
.Where(o => (o.Flag & value) == o.Flag)
.Aggregate((current, next) => new Option(options.First().Category, "Custom", current.Flag | next.Flag));
}
public bool Contains(params Option options) => Contains(options.Aggregate((current, next) => current.Flag | next.Flag).Flag);
public bool Contains(int flags) => (Flag & flags) == flags;
[DebuggerStepThrough]
public override string ToString() => $"{Category.ToString()}.{Name.ToString()}";
#region IEquatable
public bool Equals(Option other) => AutoEquality<Option>.Comparer.Equals(this, other);
public override bool Equals(object obj) => Equals(obj as Option);
public override int GetHashCode() => AutoEquality<Option>.Comparer.GetHashCode(this);
#endregion
public int CompareTo(Option other) => Comparer.Compare(this, other);
public int CompareTo(object other) => Comparer.Compare(this, other);
public static implicit operator string(Option option) => option?.ToString() ?? throw new ArgumentNullException(nameof(option));
public static implicit operator int(Option option) => option?.Flag ?? throw new ArgumentNullException(nameof(option));
public static implicit operator Option(string value) => Parse(value);
public static implicit operator Option(int value) => FromValue(value);
#region Operators
public static bool operator ==(Option left, Option right) => Comparer.Compare(left, right) == 0;
public static bool operator !=(Option left, Option right) => !(left == right);
public static bool operator <(Option left, Option right) => Comparer.Compare(left, right) < 0;
public static bool operator <=(Option left, Option right) => Comparer.Compare(left, right) <= 0;
public static bool operator >(Option left, Option right) => Comparer.Compare(left, right) > 0;
public static bool operator >=(Option left, Option right) => Comparer.Compare(left, right) >= 0;
public static Option operator |(Option left, Option right) => new Option(left.Category, "Custom", left.Flag | right.Flag);
#endregion
private class OptionComparer : IComparer<Option>, IComparer
{
public int Compare(Option left, Option right)
{
if (ReferenceEquals(left, right)) return 0;
if (ReferenceEquals(left, null)) return 1;
if (ReferenceEquals(right, null)) return -1;
return left.Flag - right.Flag;
}
public int Compare(object left, object right) => Compare(left as Option, right as Option);
}
}
This should replace the previous enum
[Flags]
public enum FeatureOptions
{
None = 0,
/// <summary>
/// When set a feature is enabled.
/// </summary>
Enabled = 1 << 0,
/// <summary>
/// When set a warning is logged when a feature is toggled.
/// </summary>
Warn = 1 << 1,
/// <summary>
/// When set feature usage statistics are logged.
/// </summary>
Telemetry = 1 << 2, // For future use
}
with
[PublicAPI]
public static class FeatureOptionsNew
{
public static readonly FeatureOption None = Option.Create<FeatureOption>(nameof(None));
/// <summary>
/// When set a feature is enabled.
/// </summary>
public static readonly FeatureOption Enable = Option.Create<FeatureOption>(nameof(Enable));
/// <summary>
/// When set a warning is logged when a feature is toggled.
/// </summary>
public static readonly FeatureOption Warn = Option.Create<FeatureOption>(nameof(Warn));
/// <summary>
/// When set feature usage statistics are logged.
/// </summary>
public static readonly FeatureOption Telemetry = Option.Create<FeatureOption>(nameof(Warn));
}
that is based on a new FeatureOption
type
public class FeatureOption : Option
{
public FeatureOption(string name, int value) : base(nameof(FeatureOption), name, value) { }
}
They can be used exactly like classic enum
s:
public class OptionTest
{
[Fact]
public void Examples()
{
Assert.Equal(new { 0, 1, 2, 4 }, new
{
FeatureOptionsNew.None,
FeatureOptionsNew.Enable,
FeatureOptionsNew.Warn,
FeatureOptionsNew.Telemetry
}.Select(o => o.Flag));
Assert.Equal(FeatureOptionsNew.Enable, FeatureOptionsNew.Enable);
Assert.NotEqual(FeatureOptionsNew.Enable, FeatureOptionsNew.Telemetry);
var oParsed = Option.Parse("Warn", FeatureOptionsNew.Enable, FeatureOptionsNew.Warn, FeatureOptionsNew.Telemetry);
Assert.Equal(FeatureOptionsNew.Warn, oParsed);
var oFromValue = Option.FromValue(3, FeatureOptionsNew.Enable, FeatureOptionsNew.Warn, FeatureOptionsNew.Telemetry);
Assert.Equal(FeatureOptionsNew.Enable | FeatureOptionsNew.Warn, oFromValue);
Assert.True(FeatureOptionsNew.None < FeatureOptionsNew.Enable);
Assert.True(FeatureOptionsNew.Enable < FeatureOptionsNew.Telemetry);
}
}
Questions
- Is this as extendable as I think it is?
- Are there any APIs missing that I didn't think of or would be convenient?
- What do you think about the automatic
Flag
maintenance and options creation?
c# api inheritance enum
c# api inheritance enum
edited Jun 16 at 16:07
dfhwze
13.1k3 gold badges22 silver badges92 bronze badges
13.1k3 gold badges22 silver badges92 bronze badges
asked May 26 at 12:02
t3chb0tt3chb0t
38.6k7 gold badges62 silver badges142 bronze badges
38.6k7 gold badges62 silver badges142 bronze badges
$begingroup$
The current commit.
$endgroup$
– t3chb0t
May 26 at 12:06
$begingroup$
Comments are not for extended discussion; this conversation has been moved to chat.
$endgroup$
– rolfl♦
May 27 at 13:15
$begingroup$
wow, I got another downvote... how so?
$endgroup$
– t3chb0t
Jul 12 at 11:50
add a comment
|
$begingroup$
The current commit.
$endgroup$
– t3chb0t
May 26 at 12:06
$begingroup$
Comments are not for extended discussion; this conversation has been moved to chat.
$endgroup$
– rolfl♦
May 27 at 13:15
$begingroup$
wow, I got another downvote... how so?
$endgroup$
– t3chb0t
Jul 12 at 11:50
$begingroup$
The current commit.
$endgroup$
– t3chb0t
May 26 at 12:06
$begingroup$
The current commit.
$endgroup$
– t3chb0t
May 26 at 12:06
$begingroup$
Comments are not for extended discussion; this conversation has been moved to chat.
$endgroup$
– rolfl♦
May 27 at 13:15
$begingroup$
Comments are not for extended discussion; this conversation has been moved to chat.
$endgroup$
– rolfl♦
May 27 at 13:15
$begingroup$
wow, I got another downvote... how so?
$endgroup$
– t3chb0t
Jul 12 at 11:50
$begingroup$
wow, I got another downvote... how so?
$endgroup$
– t3chb0t
Jul 12 at 11:50
add a comment
|
3 Answers
3
active
oldest
votes
$begingroup$
int NextFlag(string category)
I'd expect this to throw when it runs out of flags.
I really really don't like that the first flag just happens to be 0
: that depends on the order in which they are defined, and isn't written down anywhere.
Option FromValue(int value, params Option options)
I don't understand what this method is really meant to achieve... I'd expect it to throw a nicer exception when options
is null or empty (no category, so it has to fail), and it seems to do a lot of work to produce a new option with the given flag, implicitly filtering out options which are not given... I just don't get it. Shouldn't it throw if you are trying to stuff 42879 into something which only expects the last 4 bits to be set?
The Aggregate
seems like it incurs some unnecessary allocations, and I think the alternative of accumulating the flag before creating any options would be clearer. I'd also consider breaking it down a little so that each stage in the LINQ is clearer, and I'd kind of expect the name to be more useful (what I've done below will of course look awful when combined with your ToString()
).
var observedOptions = options.Where(o => (o.Flag & value) == o.Flag);
var flags = observedOptions.Aggregate(0, (current, o) => current | o.Flag));
var name = string.Join(" | ", observedOptions.OrderBy(o => o.Flag).Select(o => o.Name));
return new Option(options.First().Category, name, flags));
The flag accumulator could be its own method, shared with the Contains
method, since it seems like a meaningful task in its own right.
Misc
It should check for name reuse: this bug should throw in your example:
Telemetry = Option.Create<FeatureOption>(nameof(Warn));
Does it make sense to provide inequality comparators? Again, this depends on the order in which the flags are created to have meaning, though I'll grant this is consistent with
enum
.Option(string value) => Parse(value);
looks broken, as doesoperator Option(int value) => FromValue(value)
, because they don't provide any options from which to select.There are a few
[NotNull]
s strewn about the place, and some that appear to be missing (e.g. bothCreate
methods presumably don't returnnull
, nor shouldParse
; the parameters to the implicit operators).The comparer will happily compare
Option
s from different categories, which doesn't sound particularly meaningful. You might consider putting the check for uni-categoriyness into a new method takingparams Option
, and feed it in this instance also.You could make use of
[System.Runtime.CompilerServices.CallerMemberName]
inOption.Create<T>
, which could mitigate bugs like the misnaming ofTelemetry
.
$endgroup$
1
$begingroup$
That's a lot to improve! I'd better get to work...
$endgroup$
– t3chb0t
May 26 at 14:33
$begingroup$
Shouldn't it throw if you are trying to stuff 42879 - this one might be problematic as the language allows you to cast any number into an enum without throwing an exception... but I think it might be a good idea to change this behavior and make the new API more robust...
$endgroup$
– t3chb0t
May 27 at 9:24
$begingroup$
@t3chb0t Would it need to throw an exception or normalize to the subset that is known? For instance, if only 0 and 1 are defined flags, and the user requests 3, you might return the option corresponding to 1. If both 1 and 2 were defined, you might return the combined option 1 | 2. I am not sure what would be the best behavior for these partially known superset values.
$endgroup$
– dfhwze
May 28 at 10:08
$begingroup$
@dfhwze I currently throw when the user requests option > Max, I think this is better than returning something close to it becuase this shouldn't happen and would indicate an error. I like the second option and it looks like I, at least, partialy support it withFromValue
where I look for known options, otherwise I returnUnknown
, which is still valid because each bit is defined but that particular combination not.
$endgroup$
– t3chb0t
May 28 at 17:27
$begingroup$
@t3chb0t The only possible downside I see is if your options struct is shared in a public API as int/uint and the other party might have more flags specified as you. Not sure this is a valid use case for you, but if so, think about how to be compatible with the other party.
$endgroup$
– dfhwze
May 28 at 17:30
|
show 1 more comment
$begingroup$
Is this as extendable as I think it is?
Does it work for multi-bit flags? For instance
[Flags]
enum Modifiers : uint {
None = 0,
Private = 1,
Protected = 2,
Public = 4,
NonPublic = Private | Protected, // <- multi-bit
All = ~None
}
Are there any APIs missing that I didn't think of or would be
convinient?
BitVector32 has support for bit flags, sections, masks. Perhaps this serves your purpose better, since it is dynamic and flexible. There are no design time constraints like in an enum.
What do you think about the automatic Flag maintenance and options
creation?
It's cool, but I would have a look at existing API's how to augment it for masks and multi-bit flags.
$endgroup$
$begingroup$
It is very likely that I don't know what multibit flags are. Could give me an example?
$endgroup$
– t3chb0t
May 26 at 12:47
1
$begingroup$
@t3chb0t Here is an example for Value 'SuperUser': stackoverflow.com/questions/19582477/…
$endgroup$
– dfhwze
May 26 at 12:49
add a comment
|
$begingroup$
(self-answer)
v3
I wanted to use v2
of this code (below) to upgrade my old MimeType
that was very similar but it turned out I cannot because I need string
values (like application/json
) and not numerical ones (like 1
) (which are rarely useful anyway) so I've changed the whole thing to work with my SoftString
and replaced binary operations with HashSet
s. Alternatively this could use a generic value but currently I don't see any use for them.
[PublicAPI]
public abstract class Option
{
protected const string Unknown = nameof(Unknown);
public static readonly IImmutableList<SoftString> ReservedNames =
ImmutableList<SoftString>
.Empty
.Add(nameof(Option<Option>.None))
.Add(nameof(Option<Option>.Known));
// Disallow anyone else to use this class.
// This way we can guarantee that it is used only by the Option<T>.
private protected Option() { }
[NotNull]
public abstract SoftString Name { get; }
public abstract IImmutableSet<SoftString> Values { get; }
public abstract bool IsFlag { get; }
}
[PublicAPI]
[DebuggerDisplay(DebuggerDisplayString.DefaultNoQuotes)]
public abstract class Option<T> : Option, IEquatable<Option<T>>, IFormattable where T : Option
{
// Values are what matters for equality.
private static readonly IEqualityComparer<Option<T>> Comparer = EqualityComparerFactory<Option<T>>.Create
(
equals: (left, right) => left.Values.SetEquals(right.Values),
getHashCode: (obj) => obj.Values.GetHashCode()
);
// ReSharper disable once StaticMemberInGenericType - this is correct
private static readonly ConstructorInfo Constructor;
static Option()
{
Constructor =
typeof(T).GetConstructor(new { typeof(SoftString), typeof(IImmutableSet<SoftString>) })
?? throw DynamicException.Create
(
"ConstructorNotFound",
$"{typeof(T).ToPrettyString()} must provide a constructor with the following signature: " +
$"ctor({typeof(SoftString).ToPrettyString()}, {typeof(int).ToPrettyString()})"
);
// Always initialize "None".
var none = New(nameof(None), ImmutableHashSet<SoftString>.Empty.Add(nameof(None)));
Known = ImmutableHashSet<T>.Empty.Add(none);
}
protected Option(SoftString name, IImmutableSet<SoftString> values)
{
Name = name;
Values = values;
}
[NotNull]
public static T None => Known.Single(o => o.Name == nameof(None));
/// <summary>
/// Gets all known options ever created for this type.
/// </summary>
[NotNull]
public static IImmutableSet<T> Known { get; private set; }
/// <summary>
/// Gets options that have only a single value.
/// </summary>
[NotNull, ItemNotNull]
public static IEnumerable<T> Bits => Known.Where(o => o.IsFlag);
#region Option
public override SoftString Name { [DebuggerStepThrough] get; }
public override IImmutableSet<SoftString> Values { get; }
/// <summary>
/// Gets value indicating whether this option has only a single value.
/// </summary>
public override bool IsFlag => Values.Count == 1;
#endregion
#region Factories
public static T Create(SoftString name, params SoftString values)
{
return Create(name, values.ToImmutableHashSet());
}
[NotNull]
public static T Create(SoftString name, IImmutableSet<SoftString> values)
{
if (name.In(ReservedNames))
{
throw DynamicException.Create("ReservedOption", $"The option '{name}' is reserved and must not be created by the user.");
}
if (name.In(Known.Select(o => o.Name)))
{
throw DynamicException.Create("DuplicateOption", $"The option '{name}' is already defined.");
}
var newOption = New(name, values);
if (name == Unknown)
{
return newOption;
}
Known = Known.Add(newOption);
return newOption;
}
private static T New(SoftString name, IImmutableSet<SoftString> values)
{
return (T)Constructor.Invoke(new object
{
name,
values.Any()
? values
: ImmutableHashSet<SoftString>.Empty.Add(name)
});
}
[NotNull]
public static T CreateWithCallerName([CanBeNull] string value = default, [CallerMemberName] string name = default)
{
return Create(name, value ?? name);
}
[NotNull]
public static T FromName([NotNull] string name)
{
if (name == null) throw new ArgumentNullException(nameof(name));
return
Known.FirstOrDefault(o => o.Name == name)
?? throw DynamicException.Create("OptionOutOfRange", $"There is no such option as '{name}'.");
}
private static bool TryGetKnownOption(IEnumerable<SoftString> values, out T option)
{
if (Known.SingleOrDefault(o => o.Values.SetEquals(values)) is var knownOption && !(knownOption is null))
{
option = knownOption;
return true;
}
else
{
option = default;
return false;
}
}
#endregion
public T Set(Option<T> option) => this | option;
public T Reset(Option<T> option) => this ^ option;
[DebuggerStepThrough]
public string ToString(string format, IFormatProvider formatProvider)
{
if (format.In(new { "asc", null }, SoftString.Comparer))
{
return Values.OrderBy(x => x).Select(x => $"{x.ToString()}").Join(", ");
}
if (format.In(new { "desc" }, SoftString.Comparer))
{
return Values.OrderByDescending(x => x).Select(x => $"{x.ToString()}").Join(", ");
}
return ToString();
}
public override string ToString() => $"{this:asc}";
public bool Contains(T option) => Values.Overlaps(option.Values);
#region IEquatable
public bool Equals(Option<T> other) => Comparer.Equals(this, other);
public override bool Equals(object obj) => Equals(obj as Option<T>);
public override int GetHashCode() => Comparer.GetHashCode(this);
#endregion
#region Operators
public static implicit operator string(Option<T> option) => option?.ToString() ?? throw new ArgumentNullException(nameof(option));
public static bool operator ==(Option<T> left, Option<T> right) => Comparer.Equals(left, right);
public static bool operator !=(Option<T> left, Option<T> right) => !(left == right);
[NotNull]
public static T operator |(Option<T> left, Option<T> right)
{
var values = left.Values.Concat(right.Values).ToImmutableHashSet();
return GetKnownOrCreate(values);
}
[NotNull]
public static T operator ^(Option<T> left, Option<T> right)
{
var values = left.Values.Except(right.Values).ToImmutableHashSet();
return GetKnownOrCreate(values);
}
private static T GetKnownOrCreate(IImmutableSet<SoftString> values)
{
return
TryGetKnownOption(values, out var knownOption)
? knownOption
: Create(Unknown, values);
}
#endregion
}
v2
I have made a couple of changes so here's the summary and the improved code:
- Using
CallerMemberName
for automatic option names, however, it's still possible to create custom options with anyname. - Using generic
Option<T>
to remove theDictionary
and provide a few default properties such asNone
,All
orMax
,Bits
. - Cleaned-up naming; now parsing APIs are called
FromName
andFromValue
- Added internal set of options so that I can check whether an option is already defined and use it for other properties like
All
,Max
andBits
. - Added multi-bit support.
- Not using
BitVector32
yet... maybe later. - Added
IFormattable
interface and three formats:names
,flags
andnames+flags
. - Encapsulated operators
|
and^
respectively asSet
andReset
. - Added
Flags
property that enumerates all bits of an option.
[PublicAPI]
public abstract class Option
{
public static readonly IImmutableList<SoftString> ReservedNames =
ImmutableList<SoftString>
.Empty
.Add(nameof(Option<Option>.None))
.Add(nameof(Option<Option>.All))
.Add(nameof(Option<Option>.Max));
// Disallow anyone else to use this class.
// This way we can guarantee that it is used only by the Option<T>.
private protected Option() { }
[NotNull]
public abstract SoftString Name { get; }
public abstract int Flag { get; }
/// <summary>
/// Returns True if Option is power of two.
/// </summary>
public abstract bool IsBit { get; }
}
[PublicAPI]
[DebuggerDisplay(DebuggerDisplayString.DefaultNoQuotes)]
public abstract class Option<T> : Option, IEquatable<Option<T>>, IComparable<Option<T>>, IComparable, IFormattable where T : Option
{
protected const string Unknown = nameof(Unknown);
private static readonly OptionComparer Comparer = new OptionComparer();
private static IImmutableSet<T> Options;
static Option()
{
// Always initialize "None".
Options = ImmutableSortedSet<T>.Empty.Add(Create(nameof(None), 0));
}
protected Option(SoftString name, int flag)
{
if (GetType() != typeof(T)) throw DynamicException.Create("OptionTypeMismatch", "Option must be a type of itself.");
Name = name;
Flag = flag;
}
#region Default options
[NotNull]
public static T None => Options.First();
[NotNull]
public static T Max => Options.Last();
[NotNull]
public static IEnumerable<T> All => Options;
#endregion
[NotNull, ItemNotNull]
public static IEnumerable<T> Bits => Options.Where(o => o.IsBit);
#region Option
public override SoftString Name { [DebuggerStepThrough] get; }
[AutoEqualityProperty]
public override int Flag { [DebuggerStepThrough] get; }
public override bool IsBit => (Flag & (Flag - 1)) == 0;
#endregion
[NotNull, ItemNotNull]
public IEnumerable<T> Flags => Bits.Where(f => (Flag & f.Flag) > 0);
#region Factories
[NotNull]
public static T Create(SoftString name, T option = default)
{
if (name.In(Options.Select(o => o.Name).Concat(ReservedNames)))
{
throw DynamicException.Create("DuplicateOption", $"The option '{name}' is defined more the once.");
}
var bitCount = Options.Count(o => o.IsBit);
var newOption = Create(name, bitCount == 1 ? 1 : (bitCount - 1) << 1);
Options = Options.Add(newOption);
return newOption;
}
[NotNull]
public static T CreateWithCallerName(T option = default, [CallerMemberName] string name = default)
{
return Create(name, option);
}
private static T Create(SoftString name, IEnumerable<int> flags)
{
var flag = flags.Aggregate(0, (current, next) => current | next);
return (T)Activator.CreateInstance(typeof(T), name, flag);
}
public static T Create(SoftString name, params int flags)
{
return Create(name, flags.AsEnumerable());
}
[NotNull]
public static T FromName([NotNull] string value)
{
if (value == null) throw new ArgumentNullException(nameof(value));
return
Options.FirstOrDefault(o => o.Name == value)
?? throw DynamicException.Create("OptionOutOfRange", $"There is no such option as '{value}'.");
}
[NotNull]
public static T FromValue(int value)
{
if (value > Max.Flag)
{
throw new ArgumentOutOfRangeException(paramName: nameof(value), $"Value {value} is greater than the highest option.");
}
// Is this a known value?
if (TryGetKnownOption(value, out var knownOption))
{
return knownOption;
}
var newFlags = Bits.Where(o => (o.Flag & value) == o.Flag).Select(o => o.Flag);
return Create(Unknown, newFlags);
}
private static bool TryGetKnownOption(int flag, out T option)
{
if (Options.SingleOrDefault(o => o.Flag == flag) is var knownOption && !(knownOption is null))
{
option = knownOption;
return true;
}
else
{
option = default;
return false;
}
}
#endregion
public T Set(Option<T> option)
{
return this | option;
}
public T Reset(Option<T> option)
{
return this ^ option;
}
[DebuggerStepThrough]
public string ToString(string format, IFormatProvider formatProvider)
{
if (SoftString.Comparer.Equals(format, "names"))
{
return Flags.Select(o => $"{o.Name.ToString()}").Join(", ");
}
if (SoftString.Comparer.Equals(format, "flags"))
{
return Flags.Select(o => $"{o.Flag}").Join(", ");
}
if (SoftString.Comparer.Equals(format, "names+flags"))
{
return Flags.Select(o => $"{o.Name.ToString()}[{o.Flag}]").Join(", ");
}
return ToString();
}
public override string ToString() => $"{this:names}";
public bool Contains(T option) => Contains(option.Flag);
public bool Contains(int flags) => (Flag & flags) == flags;
public int CompareTo(Option<T> other) => Comparer.Compare(this, other);
public int CompareTo(object other) => Comparer.Compare(this, other);
#region IEquatable
public bool Equals(Option<T> other) => AutoEquality<Option<T>>.Comparer.Equals(this, other);
public override bool Equals(object obj) => Equals(obj as Option<T>);
public override int GetHashCode() => AutoEquality<Option<T>>.Comparer.GetHashCode(this);
#endregion
#region Operators
public static implicit operator string(Option<T> option) => option?.ToString() ?? throw new ArgumentNullException(nameof(option));
public static implicit operator int(Option<T> option) => option?.Flag ?? throw new ArgumentNullException(nameof(option));
public static bool operator ==(Option<T> left, Option<T> right) => Comparer.Compare(left, right) == 0;
public static bool operator !=(Option<T> left, Option<T> right) => !(left == right);
public static bool operator <(Option<T> left, Option<T> right) => Comparer.Compare(left, right) < 0;
public static bool operator <=(Option<T> left, Option<T> right) => Comparer.Compare(left, right) <= 0;
public static bool operator >(Option<T> left, Option<T> right) => Comparer.Compare(left, right) > 0;
public static bool operator >=(Option<T> left, Option<T> right) => Comparer.Compare(left, right) >= 0;
[NotNull]
public static T operator |(Option<T> left, Option<T> right) => GetKnownOrCreate(left.Flag | right.Flag);
[NotNull]
public static T operator ^(Option<T> left, Option<T> right) => GetKnownOrCreate(left.Flag ^ right.Flag);
private static T GetKnownOrCreate(int flag)
{
return
TryGetKnownOption(flag, out var knownOption)
? knownOption
: Create(Unknown, flag);
}
#endregion
private class OptionComparer : IComparer<Option<T>>, IComparer
{
public int Compare(Option<T> left, Option<T> right)
{
if (ReferenceEquals(left, right)) return 0;
if (ReferenceEquals(left, null)) return 1;
if (ReferenceEquals(right, null)) return -1;
return left.Flag - right.Flag;
}
public int Compare(object left, object right)
{
return Compare(left as Option<T>, right as Option<T>);
}
}
}
A new option-set can now be defined by deriving it from Option<T>
and adding static
properties for the desired flags:
public class FeatureOption : Option<FeatureOption>
{
public FeatureOption(SoftString name, int value) : base(name, value) { }
/// <summary>
/// When set a feature is enabled.
/// </summary>
public static readonly FeatureOption Enable = CreateWithCallerName();
/// <summary>
/// When set a warning is logged when a feature is toggled.
/// </summary>
public static readonly FeatureOption Warn = CreateWithCallerName();
/// <summary>
/// When set feature usage statistics are logged.
/// </summary>
public static readonly FeatureOption Telemetry = CreateWithCallerName();
public static readonly FeatureOption Default = CreateWithCallerName(Enable | Warn);
}
Since there is only one option-class now, tests have also become simpler.
public class OptionTest
{
[Fact]
public void Examples()
{
Assert.Equal(new { 0, 1, 2, 4 }, new
{
FeatureOption.None,
FeatureOption.Enable,
FeatureOption.Warn,
FeatureOption.Telemetry
}.Select(o => o.Flag));
Assert.Equal(FeatureOption.Enable, FeatureOption.Enable);
Assert.NotEqual(FeatureOption.Enable, FeatureOption.Telemetry);
var fromName = FeatureOption.FromName("Warn");
Assert.Equal(FeatureOption.Warn, fromName);
var fromValue = FeatureOption.FromValue(3);
var enableWarn = FeatureOption.Enable | FeatureOption.Warn;
Assert.Equal(enableWarn, fromValue);
var names = $"{enableWarn:names}";
var flags = $"{enableWarn:flags}";
var namesAndFlags = $"{enableWarn:names+flags}";
var @default = $"{enableWarn}";
Assert.True(FeatureOption.None < FeatureOption.Enable);
Assert.True(FeatureOption.Enable < FeatureOption.Telemetry);
Assert.Throws<ArgumentOutOfRangeException>(() => FeatureOption.FromValue(1000));
//Assert.ThrowsAny<DynamicException>(() => FeatureOption.Create("All", 111111));
}
}
The intended usage is:
- Logger layer where the user can define their custom log-levels
- FeatureService where the user can define their custom behaviours
- Other services that work with some default options and let the user customize it with their domain-specific flags.
$endgroup$
$begingroup$
Does a private protected constructor work? I have never seen this before.
$endgroup$
– dfhwze
Jun 16 at 16:03
1
$begingroup$
@dfhwze this is new in7.2
and the first time here where it was actually useful, see C# 7 Series, Part 5: Private Protected This way I can make sure onlyOption<T>
can access it and no other type and guarantee that nobody will break the constraintclass Option<T> : Option where T : Option
$endgroup$
– t3chb0t
Jun 16 at 16:07
add a comment
|
Your Answer
StackExchange.ifUsing("editor", function () {
StackExchange.using("externalEditor", function () {
StackExchange.using("snippets", function () {
StackExchange.snippets.init();
});
});
}, "code-snippets");
StackExchange.ready(function() {
var channelOptions = {
tags: "".split(" "),
id: "196"
};
initTagRenderer("".split(" "), "".split(" "), channelOptions);
StackExchange.using("externalEditor", function() {
// Have to fire editor after snippets, if snippets enabled
if (StackExchange.settings.snippets.snippetsEnabled) {
StackExchange.using("snippets", function() {
createEditor();
});
}
else {
createEditor();
}
});
function createEditor() {
StackExchange.prepareEditor({
heartbeatType: 'answer',
autoActivateHeartbeat: false,
convertImagesToLinks: false,
noModals: true,
showLowRepImageUploadWarning: true,
reputationToPostImages: null,
bindNavPrevention: true,
postfix: "",
imageUploader: {
brandingHtml: "Powered by u003ca class="icon-imgur-white" href="https://imgur.com/"u003eu003c/au003e",
contentPolicyHtml: "User contributions licensed under u003ca href="https://creativecommons.org/licenses/by-sa/4.0/"u003ecc by-sa 4.0 with attribution requiredu003c/au003e u003ca href="https://stackoverflow.com/legal/content-policy"u003e(content policy)u003c/au003e",
allowUrls: true
},
onDemand: true,
discardSelector: ".discard-answer"
,immediatelyShowMarkdownHelp:true
});
}
});
Sign up or log in
StackExchange.ready(function () {
StackExchange.helpers.onClickDraftSave('#login-link');
});
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
StackExchange.ready(
function () {
StackExchange.openid.initPostLogin('.new-post-login', 'https%3a%2f%2fcodereview.stackexchange.com%2fquestions%2f221057%2fgeneral-purpose-replacement-for-enum-with-flagsattribute%23new-answer', 'question_page');
}
);
Post as a guest
Required, but never shown
3 Answers
3
active
oldest
votes
3 Answers
3
active
oldest
votes
active
oldest
votes
active
oldest
votes
$begingroup$
int NextFlag(string category)
I'd expect this to throw when it runs out of flags.
I really really don't like that the first flag just happens to be 0
: that depends on the order in which they are defined, and isn't written down anywhere.
Option FromValue(int value, params Option options)
I don't understand what this method is really meant to achieve... I'd expect it to throw a nicer exception when options
is null or empty (no category, so it has to fail), and it seems to do a lot of work to produce a new option with the given flag, implicitly filtering out options which are not given... I just don't get it. Shouldn't it throw if you are trying to stuff 42879 into something which only expects the last 4 bits to be set?
The Aggregate
seems like it incurs some unnecessary allocations, and I think the alternative of accumulating the flag before creating any options would be clearer. I'd also consider breaking it down a little so that each stage in the LINQ is clearer, and I'd kind of expect the name to be more useful (what I've done below will of course look awful when combined with your ToString()
).
var observedOptions = options.Where(o => (o.Flag & value) == o.Flag);
var flags = observedOptions.Aggregate(0, (current, o) => current | o.Flag));
var name = string.Join(" | ", observedOptions.OrderBy(o => o.Flag).Select(o => o.Name));
return new Option(options.First().Category, name, flags));
The flag accumulator could be its own method, shared with the Contains
method, since it seems like a meaningful task in its own right.
Misc
It should check for name reuse: this bug should throw in your example:
Telemetry = Option.Create<FeatureOption>(nameof(Warn));
Does it make sense to provide inequality comparators? Again, this depends on the order in which the flags are created to have meaning, though I'll grant this is consistent with
enum
.Option(string value) => Parse(value);
looks broken, as doesoperator Option(int value) => FromValue(value)
, because they don't provide any options from which to select.There are a few
[NotNull]
s strewn about the place, and some that appear to be missing (e.g. bothCreate
methods presumably don't returnnull
, nor shouldParse
; the parameters to the implicit operators).The comparer will happily compare
Option
s from different categories, which doesn't sound particularly meaningful. You might consider putting the check for uni-categoriyness into a new method takingparams Option
, and feed it in this instance also.You could make use of
[System.Runtime.CompilerServices.CallerMemberName]
inOption.Create<T>
, which could mitigate bugs like the misnaming ofTelemetry
.
$endgroup$
1
$begingroup$
That's a lot to improve! I'd better get to work...
$endgroup$
– t3chb0t
May 26 at 14:33
$begingroup$
Shouldn't it throw if you are trying to stuff 42879 - this one might be problematic as the language allows you to cast any number into an enum without throwing an exception... but I think it might be a good idea to change this behavior and make the new API more robust...
$endgroup$
– t3chb0t
May 27 at 9:24
$begingroup$
@t3chb0t Would it need to throw an exception or normalize to the subset that is known? For instance, if only 0 and 1 are defined flags, and the user requests 3, you might return the option corresponding to 1. If both 1 and 2 were defined, you might return the combined option 1 | 2. I am not sure what would be the best behavior for these partially known superset values.
$endgroup$
– dfhwze
May 28 at 10:08
$begingroup$
@dfhwze I currently throw when the user requests option > Max, I think this is better than returning something close to it becuase this shouldn't happen and would indicate an error. I like the second option and it looks like I, at least, partialy support it withFromValue
where I look for known options, otherwise I returnUnknown
, which is still valid because each bit is defined but that particular combination not.
$endgroup$
– t3chb0t
May 28 at 17:27
$begingroup$
@t3chb0t The only possible downside I see is if your options struct is shared in a public API as int/uint and the other party might have more flags specified as you. Not sure this is a valid use case for you, but if so, think about how to be compatible with the other party.
$endgroup$
– dfhwze
May 28 at 17:30
|
show 1 more comment
$begingroup$
int NextFlag(string category)
I'd expect this to throw when it runs out of flags.
I really really don't like that the first flag just happens to be 0
: that depends on the order in which they are defined, and isn't written down anywhere.
Option FromValue(int value, params Option options)
I don't understand what this method is really meant to achieve... I'd expect it to throw a nicer exception when options
is null or empty (no category, so it has to fail), and it seems to do a lot of work to produce a new option with the given flag, implicitly filtering out options which are not given... I just don't get it. Shouldn't it throw if you are trying to stuff 42879 into something which only expects the last 4 bits to be set?
The Aggregate
seems like it incurs some unnecessary allocations, and I think the alternative of accumulating the flag before creating any options would be clearer. I'd also consider breaking it down a little so that each stage in the LINQ is clearer, and I'd kind of expect the name to be more useful (what I've done below will of course look awful when combined with your ToString()
).
var observedOptions = options.Where(o => (o.Flag & value) == o.Flag);
var flags = observedOptions.Aggregate(0, (current, o) => current | o.Flag));
var name = string.Join(" | ", observedOptions.OrderBy(o => o.Flag).Select(o => o.Name));
return new Option(options.First().Category, name, flags));
The flag accumulator could be its own method, shared with the Contains
method, since it seems like a meaningful task in its own right.
Misc
It should check for name reuse: this bug should throw in your example:
Telemetry = Option.Create<FeatureOption>(nameof(Warn));
Does it make sense to provide inequality comparators? Again, this depends on the order in which the flags are created to have meaning, though I'll grant this is consistent with
enum
.Option(string value) => Parse(value);
looks broken, as doesoperator Option(int value) => FromValue(value)
, because they don't provide any options from which to select.There are a few
[NotNull]
s strewn about the place, and some that appear to be missing (e.g. bothCreate
methods presumably don't returnnull
, nor shouldParse
; the parameters to the implicit operators).The comparer will happily compare
Option
s from different categories, which doesn't sound particularly meaningful. You might consider putting the check for uni-categoriyness into a new method takingparams Option
, and feed it in this instance also.You could make use of
[System.Runtime.CompilerServices.CallerMemberName]
inOption.Create<T>
, which could mitigate bugs like the misnaming ofTelemetry
.
$endgroup$
1
$begingroup$
That's a lot to improve! I'd better get to work...
$endgroup$
– t3chb0t
May 26 at 14:33
$begingroup$
Shouldn't it throw if you are trying to stuff 42879 - this one might be problematic as the language allows you to cast any number into an enum without throwing an exception... but I think it might be a good idea to change this behavior and make the new API more robust...
$endgroup$
– t3chb0t
May 27 at 9:24
$begingroup$
@t3chb0t Would it need to throw an exception or normalize to the subset that is known? For instance, if only 0 and 1 are defined flags, and the user requests 3, you might return the option corresponding to 1. If both 1 and 2 were defined, you might return the combined option 1 | 2. I am not sure what would be the best behavior for these partially known superset values.
$endgroup$
– dfhwze
May 28 at 10:08
$begingroup$
@dfhwze I currently throw when the user requests option > Max, I think this is better than returning something close to it becuase this shouldn't happen and would indicate an error. I like the second option and it looks like I, at least, partialy support it withFromValue
where I look for known options, otherwise I returnUnknown
, which is still valid because each bit is defined but that particular combination not.
$endgroup$
– t3chb0t
May 28 at 17:27
$begingroup$
@t3chb0t The only possible downside I see is if your options struct is shared in a public API as int/uint and the other party might have more flags specified as you. Not sure this is a valid use case for you, but if so, think about how to be compatible with the other party.
$endgroup$
– dfhwze
May 28 at 17:30
|
show 1 more comment
$begingroup$
int NextFlag(string category)
I'd expect this to throw when it runs out of flags.
I really really don't like that the first flag just happens to be 0
: that depends on the order in which they are defined, and isn't written down anywhere.
Option FromValue(int value, params Option options)
I don't understand what this method is really meant to achieve... I'd expect it to throw a nicer exception when options
is null or empty (no category, so it has to fail), and it seems to do a lot of work to produce a new option with the given flag, implicitly filtering out options which are not given... I just don't get it. Shouldn't it throw if you are trying to stuff 42879 into something which only expects the last 4 bits to be set?
The Aggregate
seems like it incurs some unnecessary allocations, and I think the alternative of accumulating the flag before creating any options would be clearer. I'd also consider breaking it down a little so that each stage in the LINQ is clearer, and I'd kind of expect the name to be more useful (what I've done below will of course look awful when combined with your ToString()
).
var observedOptions = options.Where(o => (o.Flag & value) == o.Flag);
var flags = observedOptions.Aggregate(0, (current, o) => current | o.Flag));
var name = string.Join(" | ", observedOptions.OrderBy(o => o.Flag).Select(o => o.Name));
return new Option(options.First().Category, name, flags));
The flag accumulator could be its own method, shared with the Contains
method, since it seems like a meaningful task in its own right.
Misc
It should check for name reuse: this bug should throw in your example:
Telemetry = Option.Create<FeatureOption>(nameof(Warn));
Does it make sense to provide inequality comparators? Again, this depends on the order in which the flags are created to have meaning, though I'll grant this is consistent with
enum
.Option(string value) => Parse(value);
looks broken, as doesoperator Option(int value) => FromValue(value)
, because they don't provide any options from which to select.There are a few
[NotNull]
s strewn about the place, and some that appear to be missing (e.g. bothCreate
methods presumably don't returnnull
, nor shouldParse
; the parameters to the implicit operators).The comparer will happily compare
Option
s from different categories, which doesn't sound particularly meaningful. You might consider putting the check for uni-categoriyness into a new method takingparams Option
, and feed it in this instance also.You could make use of
[System.Runtime.CompilerServices.CallerMemberName]
inOption.Create<T>
, which could mitigate bugs like the misnaming ofTelemetry
.
$endgroup$
int NextFlag(string category)
I'd expect this to throw when it runs out of flags.
I really really don't like that the first flag just happens to be 0
: that depends on the order in which they are defined, and isn't written down anywhere.
Option FromValue(int value, params Option options)
I don't understand what this method is really meant to achieve... I'd expect it to throw a nicer exception when options
is null or empty (no category, so it has to fail), and it seems to do a lot of work to produce a new option with the given flag, implicitly filtering out options which are not given... I just don't get it. Shouldn't it throw if you are trying to stuff 42879 into something which only expects the last 4 bits to be set?
The Aggregate
seems like it incurs some unnecessary allocations, and I think the alternative of accumulating the flag before creating any options would be clearer. I'd also consider breaking it down a little so that each stage in the LINQ is clearer, and I'd kind of expect the name to be more useful (what I've done below will of course look awful when combined with your ToString()
).
var observedOptions = options.Where(o => (o.Flag & value) == o.Flag);
var flags = observedOptions.Aggregate(0, (current, o) => current | o.Flag));
var name = string.Join(" | ", observedOptions.OrderBy(o => o.Flag).Select(o => o.Name));
return new Option(options.First().Category, name, flags));
The flag accumulator could be its own method, shared with the Contains
method, since it seems like a meaningful task in its own right.
Misc
It should check for name reuse: this bug should throw in your example:
Telemetry = Option.Create<FeatureOption>(nameof(Warn));
Does it make sense to provide inequality comparators? Again, this depends on the order in which the flags are created to have meaning, though I'll grant this is consistent with
enum
.Option(string value) => Parse(value);
looks broken, as doesoperator Option(int value) => FromValue(value)
, because they don't provide any options from which to select.There are a few
[NotNull]
s strewn about the place, and some that appear to be missing (e.g. bothCreate
methods presumably don't returnnull
, nor shouldParse
; the parameters to the implicit operators).The comparer will happily compare
Option
s from different categories, which doesn't sound particularly meaningful. You might consider putting the check for uni-categoriyness into a new method takingparams Option
, and feed it in this instance also.You could make use of
[System.Runtime.CompilerServices.CallerMemberName]
inOption.Create<T>
, which could mitigate bugs like the misnaming ofTelemetry
.
edited May 26 at 13:29
answered May 26 at 13:04
VisualMelonVisualMelon
6,71215 silver badges44 bronze badges
6,71215 silver badges44 bronze badges
1
$begingroup$
That's a lot to improve! I'd better get to work...
$endgroup$
– t3chb0t
May 26 at 14:33
$begingroup$
Shouldn't it throw if you are trying to stuff 42879 - this one might be problematic as the language allows you to cast any number into an enum without throwing an exception... but I think it might be a good idea to change this behavior and make the new API more robust...
$endgroup$
– t3chb0t
May 27 at 9:24
$begingroup$
@t3chb0t Would it need to throw an exception or normalize to the subset that is known? For instance, if only 0 and 1 are defined flags, and the user requests 3, you might return the option corresponding to 1. If both 1 and 2 were defined, you might return the combined option 1 | 2. I am not sure what would be the best behavior for these partially known superset values.
$endgroup$
– dfhwze
May 28 at 10:08
$begingroup$
@dfhwze I currently throw when the user requests option > Max, I think this is better than returning something close to it becuase this shouldn't happen and would indicate an error. I like the second option and it looks like I, at least, partialy support it withFromValue
where I look for known options, otherwise I returnUnknown
, which is still valid because each bit is defined but that particular combination not.
$endgroup$
– t3chb0t
May 28 at 17:27
$begingroup$
@t3chb0t The only possible downside I see is if your options struct is shared in a public API as int/uint and the other party might have more flags specified as you. Not sure this is a valid use case for you, but if so, think about how to be compatible with the other party.
$endgroup$
– dfhwze
May 28 at 17:30
|
show 1 more comment
1
$begingroup$
That's a lot to improve! I'd better get to work...
$endgroup$
– t3chb0t
May 26 at 14:33
$begingroup$
Shouldn't it throw if you are trying to stuff 42879 - this one might be problematic as the language allows you to cast any number into an enum without throwing an exception... but I think it might be a good idea to change this behavior and make the new API more robust...
$endgroup$
– t3chb0t
May 27 at 9:24
$begingroup$
@t3chb0t Would it need to throw an exception or normalize to the subset that is known? For instance, if only 0 and 1 are defined flags, and the user requests 3, you might return the option corresponding to 1. If both 1 and 2 were defined, you might return the combined option 1 | 2. I am not sure what would be the best behavior for these partially known superset values.
$endgroup$
– dfhwze
May 28 at 10:08
$begingroup$
@dfhwze I currently throw when the user requests option > Max, I think this is better than returning something close to it becuase this shouldn't happen and would indicate an error. I like the second option and it looks like I, at least, partialy support it withFromValue
where I look for known options, otherwise I returnUnknown
, which is still valid because each bit is defined but that particular combination not.
$endgroup$
– t3chb0t
May 28 at 17:27
$begingroup$
@t3chb0t The only possible downside I see is if your options struct is shared in a public API as int/uint and the other party might have more flags specified as you. Not sure this is a valid use case for you, but if so, think about how to be compatible with the other party.
$endgroup$
– dfhwze
May 28 at 17:30
1
1
$begingroup$
That's a lot to improve! I'd better get to work...
$endgroup$
– t3chb0t
May 26 at 14:33
$begingroup$
That's a lot to improve! I'd better get to work...
$endgroup$
– t3chb0t
May 26 at 14:33
$begingroup$
Shouldn't it throw if you are trying to stuff 42879 - this one might be problematic as the language allows you to cast any number into an enum without throwing an exception... but I think it might be a good idea to change this behavior and make the new API more robust...
$endgroup$
– t3chb0t
May 27 at 9:24
$begingroup$
Shouldn't it throw if you are trying to stuff 42879 - this one might be problematic as the language allows you to cast any number into an enum without throwing an exception... but I think it might be a good idea to change this behavior and make the new API more robust...
$endgroup$
– t3chb0t
May 27 at 9:24
$begingroup$
@t3chb0t Would it need to throw an exception or normalize to the subset that is known? For instance, if only 0 and 1 are defined flags, and the user requests 3, you might return the option corresponding to 1. If both 1 and 2 were defined, you might return the combined option 1 | 2. I am not sure what would be the best behavior for these partially known superset values.
$endgroup$
– dfhwze
May 28 at 10:08
$begingroup$
@t3chb0t Would it need to throw an exception or normalize to the subset that is known? For instance, if only 0 and 1 are defined flags, and the user requests 3, you might return the option corresponding to 1. If both 1 and 2 were defined, you might return the combined option 1 | 2. I am not sure what would be the best behavior for these partially known superset values.
$endgroup$
– dfhwze
May 28 at 10:08
$begingroup$
@dfhwze I currently throw when the user requests option > Max, I think this is better than returning something close to it becuase this shouldn't happen and would indicate an error. I like the second option and it looks like I, at least, partialy support it with
FromValue
where I look for known options, otherwise I return Unknown
, which is still valid because each bit is defined but that particular combination not.$endgroup$
– t3chb0t
May 28 at 17:27
$begingroup$
@dfhwze I currently throw when the user requests option > Max, I think this is better than returning something close to it becuase this shouldn't happen and would indicate an error. I like the second option and it looks like I, at least, partialy support it with
FromValue
where I look for known options, otherwise I return Unknown
, which is still valid because each bit is defined but that particular combination not.$endgroup$
– t3chb0t
May 28 at 17:27
$begingroup$
@t3chb0t The only possible downside I see is if your options struct is shared in a public API as int/uint and the other party might have more flags specified as you. Not sure this is a valid use case for you, but if so, think about how to be compatible with the other party.
$endgroup$
– dfhwze
May 28 at 17:30
$begingroup$
@t3chb0t The only possible downside I see is if your options struct is shared in a public API as int/uint and the other party might have more flags specified as you. Not sure this is a valid use case for you, but if so, think about how to be compatible with the other party.
$endgroup$
– dfhwze
May 28 at 17:30
|
show 1 more comment
$begingroup$
Is this as extendable as I think it is?
Does it work for multi-bit flags? For instance
[Flags]
enum Modifiers : uint {
None = 0,
Private = 1,
Protected = 2,
Public = 4,
NonPublic = Private | Protected, // <- multi-bit
All = ~None
}
Are there any APIs missing that I didn't think of or would be
convinient?
BitVector32 has support for bit flags, sections, masks. Perhaps this serves your purpose better, since it is dynamic and flexible. There are no design time constraints like in an enum.
What do you think about the automatic Flag maintenance and options
creation?
It's cool, but I would have a look at existing API's how to augment it for masks and multi-bit flags.
$endgroup$
$begingroup$
It is very likely that I don't know what multibit flags are. Could give me an example?
$endgroup$
– t3chb0t
May 26 at 12:47
1
$begingroup$
@t3chb0t Here is an example for Value 'SuperUser': stackoverflow.com/questions/19582477/…
$endgroup$
– dfhwze
May 26 at 12:49
add a comment
|
$begingroup$
Is this as extendable as I think it is?
Does it work for multi-bit flags? For instance
[Flags]
enum Modifiers : uint {
None = 0,
Private = 1,
Protected = 2,
Public = 4,
NonPublic = Private | Protected, // <- multi-bit
All = ~None
}
Are there any APIs missing that I didn't think of or would be
convinient?
BitVector32 has support for bit flags, sections, masks. Perhaps this serves your purpose better, since it is dynamic and flexible. There are no design time constraints like in an enum.
What do you think about the automatic Flag maintenance and options
creation?
It's cool, but I would have a look at existing API's how to augment it for masks and multi-bit flags.
$endgroup$
$begingroup$
It is very likely that I don't know what multibit flags are. Could give me an example?
$endgroup$
– t3chb0t
May 26 at 12:47
1
$begingroup$
@t3chb0t Here is an example for Value 'SuperUser': stackoverflow.com/questions/19582477/…
$endgroup$
– dfhwze
May 26 at 12:49
add a comment
|
$begingroup$
Is this as extendable as I think it is?
Does it work for multi-bit flags? For instance
[Flags]
enum Modifiers : uint {
None = 0,
Private = 1,
Protected = 2,
Public = 4,
NonPublic = Private | Protected, // <- multi-bit
All = ~None
}
Are there any APIs missing that I didn't think of or would be
convinient?
BitVector32 has support for bit flags, sections, masks. Perhaps this serves your purpose better, since it is dynamic and flexible. There are no design time constraints like in an enum.
What do you think about the automatic Flag maintenance and options
creation?
It's cool, but I would have a look at existing API's how to augment it for masks and multi-bit flags.
$endgroup$
Is this as extendable as I think it is?
Does it work for multi-bit flags? For instance
[Flags]
enum Modifiers : uint {
None = 0,
Private = 1,
Protected = 2,
Public = 4,
NonPublic = Private | Protected, // <- multi-bit
All = ~None
}
Are there any APIs missing that I didn't think of or would be
convinient?
BitVector32 has support for bit flags, sections, masks. Perhaps this serves your purpose better, since it is dynamic and flexible. There are no design time constraints like in an enum.
What do you think about the automatic Flag maintenance and options
creation?
It's cool, but I would have a look at existing API's how to augment it for masks and multi-bit flags.
edited May 26 at 12:58
answered May 26 at 12:44
dfhwzedfhwze
13.1k3 gold badges22 silver badges92 bronze badges
13.1k3 gold badges22 silver badges92 bronze badges
$begingroup$
It is very likely that I don't know what multibit flags are. Could give me an example?
$endgroup$
– t3chb0t
May 26 at 12:47
1
$begingroup$
@t3chb0t Here is an example for Value 'SuperUser': stackoverflow.com/questions/19582477/…
$endgroup$
– dfhwze
May 26 at 12:49
add a comment
|
$begingroup$
It is very likely that I don't know what multibit flags are. Could give me an example?
$endgroup$
– t3chb0t
May 26 at 12:47
1
$begingroup$
@t3chb0t Here is an example for Value 'SuperUser': stackoverflow.com/questions/19582477/…
$endgroup$
– dfhwze
May 26 at 12:49
$begingroup$
It is very likely that I don't know what multibit flags are. Could give me an example?
$endgroup$
– t3chb0t
May 26 at 12:47
$begingroup$
It is very likely that I don't know what multibit flags are. Could give me an example?
$endgroup$
– t3chb0t
May 26 at 12:47
1
1
$begingroup$
@t3chb0t Here is an example for Value 'SuperUser': stackoverflow.com/questions/19582477/…
$endgroup$
– dfhwze
May 26 at 12:49
$begingroup$
@t3chb0t Here is an example for Value 'SuperUser': stackoverflow.com/questions/19582477/…
$endgroup$
– dfhwze
May 26 at 12:49
add a comment
|
$begingroup$
(self-answer)
v3
I wanted to use v2
of this code (below) to upgrade my old MimeType
that was very similar but it turned out I cannot because I need string
values (like application/json
) and not numerical ones (like 1
) (which are rarely useful anyway) so I've changed the whole thing to work with my SoftString
and replaced binary operations with HashSet
s. Alternatively this could use a generic value but currently I don't see any use for them.
[PublicAPI]
public abstract class Option
{
protected const string Unknown = nameof(Unknown);
public static readonly IImmutableList<SoftString> ReservedNames =
ImmutableList<SoftString>
.Empty
.Add(nameof(Option<Option>.None))
.Add(nameof(Option<Option>.Known));
// Disallow anyone else to use this class.
// This way we can guarantee that it is used only by the Option<T>.
private protected Option() { }
[NotNull]
public abstract SoftString Name { get; }
public abstract IImmutableSet<SoftString> Values { get; }
public abstract bool IsFlag { get; }
}
[PublicAPI]
[DebuggerDisplay(DebuggerDisplayString.DefaultNoQuotes)]
public abstract class Option<T> : Option, IEquatable<Option<T>>, IFormattable where T : Option
{
// Values are what matters for equality.
private static readonly IEqualityComparer<Option<T>> Comparer = EqualityComparerFactory<Option<T>>.Create
(
equals: (left, right) => left.Values.SetEquals(right.Values),
getHashCode: (obj) => obj.Values.GetHashCode()
);
// ReSharper disable once StaticMemberInGenericType - this is correct
private static readonly ConstructorInfo Constructor;
static Option()
{
Constructor =
typeof(T).GetConstructor(new { typeof(SoftString), typeof(IImmutableSet<SoftString>) })
?? throw DynamicException.Create
(
"ConstructorNotFound",
$"{typeof(T).ToPrettyString()} must provide a constructor with the following signature: " +
$"ctor({typeof(SoftString).ToPrettyString()}, {typeof(int).ToPrettyString()})"
);
// Always initialize "None".
var none = New(nameof(None), ImmutableHashSet<SoftString>.Empty.Add(nameof(None)));
Known = ImmutableHashSet<T>.Empty.Add(none);
}
protected Option(SoftString name, IImmutableSet<SoftString> values)
{
Name = name;
Values = values;
}
[NotNull]
public static T None => Known.Single(o => o.Name == nameof(None));
/// <summary>
/// Gets all known options ever created for this type.
/// </summary>
[NotNull]
public static IImmutableSet<T> Known { get; private set; }
/// <summary>
/// Gets options that have only a single value.
/// </summary>
[NotNull, ItemNotNull]
public static IEnumerable<T> Bits => Known.Where(o => o.IsFlag);
#region Option
public override SoftString Name { [DebuggerStepThrough] get; }
public override IImmutableSet<SoftString> Values { get; }
/// <summary>
/// Gets value indicating whether this option has only a single value.
/// </summary>
public override bool IsFlag => Values.Count == 1;
#endregion
#region Factories
public static T Create(SoftString name, params SoftString values)
{
return Create(name, values.ToImmutableHashSet());
}
[NotNull]
public static T Create(SoftString name, IImmutableSet<SoftString> values)
{
if (name.In(ReservedNames))
{
throw DynamicException.Create("ReservedOption", $"The option '{name}' is reserved and must not be created by the user.");
}
if (name.In(Known.Select(o => o.Name)))
{
throw DynamicException.Create("DuplicateOption", $"The option '{name}' is already defined.");
}
var newOption = New(name, values);
if (name == Unknown)
{
return newOption;
}
Known = Known.Add(newOption);
return newOption;
}
private static T New(SoftString name, IImmutableSet<SoftString> values)
{
return (T)Constructor.Invoke(new object
{
name,
values.Any()
? values
: ImmutableHashSet<SoftString>.Empty.Add(name)
});
}
[NotNull]
public static T CreateWithCallerName([CanBeNull] string value = default, [CallerMemberName] string name = default)
{
return Create(name, value ?? name);
}
[NotNull]
public static T FromName([NotNull] string name)
{
if (name == null) throw new ArgumentNullException(nameof(name));
return
Known.FirstOrDefault(o => o.Name == name)
?? throw DynamicException.Create("OptionOutOfRange", $"There is no such option as '{name}'.");
}
private static bool TryGetKnownOption(IEnumerable<SoftString> values, out T option)
{
if (Known.SingleOrDefault(o => o.Values.SetEquals(values)) is var knownOption && !(knownOption is null))
{
option = knownOption;
return true;
}
else
{
option = default;
return false;
}
}
#endregion
public T Set(Option<T> option) => this | option;
public T Reset(Option<T> option) => this ^ option;
[DebuggerStepThrough]
public string ToString(string format, IFormatProvider formatProvider)
{
if (format.In(new { "asc", null }, SoftString.Comparer))
{
return Values.OrderBy(x => x).Select(x => $"{x.ToString()}").Join(", ");
}
if (format.In(new { "desc" }, SoftString.Comparer))
{
return Values.OrderByDescending(x => x).Select(x => $"{x.ToString()}").Join(", ");
}
return ToString();
}
public override string ToString() => $"{this:asc}";
public bool Contains(T option) => Values.Overlaps(option.Values);
#region IEquatable
public bool Equals(Option<T> other) => Comparer.Equals(this, other);
public override bool Equals(object obj) => Equals(obj as Option<T>);
public override int GetHashCode() => Comparer.GetHashCode(this);
#endregion
#region Operators
public static implicit operator string(Option<T> option) => option?.ToString() ?? throw new ArgumentNullException(nameof(option));
public static bool operator ==(Option<T> left, Option<T> right) => Comparer.Equals(left, right);
public static bool operator !=(Option<T> left, Option<T> right) => !(left == right);
[NotNull]
public static T operator |(Option<T> left, Option<T> right)
{
var values = left.Values.Concat(right.Values).ToImmutableHashSet();
return GetKnownOrCreate(values);
}
[NotNull]
public static T operator ^(Option<T> left, Option<T> right)
{
var values = left.Values.Except(right.Values).ToImmutableHashSet();
return GetKnownOrCreate(values);
}
private static T GetKnownOrCreate(IImmutableSet<SoftString> values)
{
return
TryGetKnownOption(values, out var knownOption)
? knownOption
: Create(Unknown, values);
}
#endregion
}
v2
I have made a couple of changes so here's the summary and the improved code:
- Using
CallerMemberName
for automatic option names, however, it's still possible to create custom options with anyname. - Using generic
Option<T>
to remove theDictionary
and provide a few default properties such asNone
,All
orMax
,Bits
. - Cleaned-up naming; now parsing APIs are called
FromName
andFromValue
- Added internal set of options so that I can check whether an option is already defined and use it for other properties like
All
,Max
andBits
. - Added multi-bit support.
- Not using
BitVector32
yet... maybe later. - Added
IFormattable
interface and three formats:names
,flags
andnames+flags
. - Encapsulated operators
|
and^
respectively asSet
andReset
. - Added
Flags
property that enumerates all bits of an option.
[PublicAPI]
public abstract class Option
{
public static readonly IImmutableList<SoftString> ReservedNames =
ImmutableList<SoftString>
.Empty
.Add(nameof(Option<Option>.None))
.Add(nameof(Option<Option>.All))
.Add(nameof(Option<Option>.Max));
// Disallow anyone else to use this class.
// This way we can guarantee that it is used only by the Option<T>.
private protected Option() { }
[NotNull]
public abstract SoftString Name { get; }
public abstract int Flag { get; }
/// <summary>
/// Returns True if Option is power of two.
/// </summary>
public abstract bool IsBit { get; }
}
[PublicAPI]
[DebuggerDisplay(DebuggerDisplayString.DefaultNoQuotes)]
public abstract class Option<T> : Option, IEquatable<Option<T>>, IComparable<Option<T>>, IComparable, IFormattable where T : Option
{
protected const string Unknown = nameof(Unknown);
private static readonly OptionComparer Comparer = new OptionComparer();
private static IImmutableSet<T> Options;
static Option()
{
// Always initialize "None".
Options = ImmutableSortedSet<T>.Empty.Add(Create(nameof(None), 0));
}
protected Option(SoftString name, int flag)
{
if (GetType() != typeof(T)) throw DynamicException.Create("OptionTypeMismatch", "Option must be a type of itself.");
Name = name;
Flag = flag;
}
#region Default options
[NotNull]
public static T None => Options.First();
[NotNull]
public static T Max => Options.Last();
[NotNull]
public static IEnumerable<T> All => Options;
#endregion
[NotNull, ItemNotNull]
public static IEnumerable<T> Bits => Options.Where(o => o.IsBit);
#region Option
public override SoftString Name { [DebuggerStepThrough] get; }
[AutoEqualityProperty]
public override int Flag { [DebuggerStepThrough] get; }
public override bool IsBit => (Flag & (Flag - 1)) == 0;
#endregion
[NotNull, ItemNotNull]
public IEnumerable<T> Flags => Bits.Where(f => (Flag & f.Flag) > 0);
#region Factories
[NotNull]
public static T Create(SoftString name, T option = default)
{
if (name.In(Options.Select(o => o.Name).Concat(ReservedNames)))
{
throw DynamicException.Create("DuplicateOption", $"The option '{name}' is defined more the once.");
}
var bitCount = Options.Count(o => o.IsBit);
var newOption = Create(name, bitCount == 1 ? 1 : (bitCount - 1) << 1);
Options = Options.Add(newOption);
return newOption;
}
[NotNull]
public static T CreateWithCallerName(T option = default, [CallerMemberName] string name = default)
{
return Create(name, option);
}
private static T Create(SoftString name, IEnumerable<int> flags)
{
var flag = flags.Aggregate(0, (current, next) => current | next);
return (T)Activator.CreateInstance(typeof(T), name, flag);
}
public static T Create(SoftString name, params int flags)
{
return Create(name, flags.AsEnumerable());
}
[NotNull]
public static T FromName([NotNull] string value)
{
if (value == null) throw new ArgumentNullException(nameof(value));
return
Options.FirstOrDefault(o => o.Name == value)
?? throw DynamicException.Create("OptionOutOfRange", $"There is no such option as '{value}'.");
}
[NotNull]
public static T FromValue(int value)
{
if (value > Max.Flag)
{
throw new ArgumentOutOfRangeException(paramName: nameof(value), $"Value {value} is greater than the highest option.");
}
// Is this a known value?
if (TryGetKnownOption(value, out var knownOption))
{
return knownOption;
}
var newFlags = Bits.Where(o => (o.Flag & value) == o.Flag).Select(o => o.Flag);
return Create(Unknown, newFlags);
}
private static bool TryGetKnownOption(int flag, out T option)
{
if (Options.SingleOrDefault(o => o.Flag == flag) is var knownOption && !(knownOption is null))
{
option = knownOption;
return true;
}
else
{
option = default;
return false;
}
}
#endregion
public T Set(Option<T> option)
{
return this | option;
}
public T Reset(Option<T> option)
{
return this ^ option;
}
[DebuggerStepThrough]
public string ToString(string format, IFormatProvider formatProvider)
{
if (SoftString.Comparer.Equals(format, "names"))
{
return Flags.Select(o => $"{o.Name.ToString()}").Join(", ");
}
if (SoftString.Comparer.Equals(format, "flags"))
{
return Flags.Select(o => $"{o.Flag}").Join(", ");
}
if (SoftString.Comparer.Equals(format, "names+flags"))
{
return Flags.Select(o => $"{o.Name.ToString()}[{o.Flag}]").Join(", ");
}
return ToString();
}
public override string ToString() => $"{this:names}";
public bool Contains(T option) => Contains(option.Flag);
public bool Contains(int flags) => (Flag & flags) == flags;
public int CompareTo(Option<T> other) => Comparer.Compare(this, other);
public int CompareTo(object other) => Comparer.Compare(this, other);
#region IEquatable
public bool Equals(Option<T> other) => AutoEquality<Option<T>>.Comparer.Equals(this, other);
public override bool Equals(object obj) => Equals(obj as Option<T>);
public override int GetHashCode() => AutoEquality<Option<T>>.Comparer.GetHashCode(this);
#endregion
#region Operators
public static implicit operator string(Option<T> option) => option?.ToString() ?? throw new ArgumentNullException(nameof(option));
public static implicit operator int(Option<T> option) => option?.Flag ?? throw new ArgumentNullException(nameof(option));
public static bool operator ==(Option<T> left, Option<T> right) => Comparer.Compare(left, right) == 0;
public static bool operator !=(Option<T> left, Option<T> right) => !(left == right);
public static bool operator <(Option<T> left, Option<T> right) => Comparer.Compare(left, right) < 0;
public static bool operator <=(Option<T> left, Option<T> right) => Comparer.Compare(left, right) <= 0;
public static bool operator >(Option<T> left, Option<T> right) => Comparer.Compare(left, right) > 0;
public static bool operator >=(Option<T> left, Option<T> right) => Comparer.Compare(left, right) >= 0;
[NotNull]
public static T operator |(Option<T> left, Option<T> right) => GetKnownOrCreate(left.Flag | right.Flag);
[NotNull]
public static T operator ^(Option<T> left, Option<T> right) => GetKnownOrCreate(left.Flag ^ right.Flag);
private static T GetKnownOrCreate(int flag)
{
return
TryGetKnownOption(flag, out var knownOption)
? knownOption
: Create(Unknown, flag);
}
#endregion
private class OptionComparer : IComparer<Option<T>>, IComparer
{
public int Compare(Option<T> left, Option<T> right)
{
if (ReferenceEquals(left, right)) return 0;
if (ReferenceEquals(left, null)) return 1;
if (ReferenceEquals(right, null)) return -1;
return left.Flag - right.Flag;
}
public int Compare(object left, object right)
{
return Compare(left as Option<T>, right as Option<T>);
}
}
}
A new option-set can now be defined by deriving it from Option<T>
and adding static
properties for the desired flags:
public class FeatureOption : Option<FeatureOption>
{
public FeatureOption(SoftString name, int value) : base(name, value) { }
/// <summary>
/// When set a feature is enabled.
/// </summary>
public static readonly FeatureOption Enable = CreateWithCallerName();
/// <summary>
/// When set a warning is logged when a feature is toggled.
/// </summary>
public static readonly FeatureOption Warn = CreateWithCallerName();
/// <summary>
/// When set feature usage statistics are logged.
/// </summary>
public static readonly FeatureOption Telemetry = CreateWithCallerName();
public static readonly FeatureOption Default = CreateWithCallerName(Enable | Warn);
}
Since there is only one option-class now, tests have also become simpler.
public class OptionTest
{
[Fact]
public void Examples()
{
Assert.Equal(new { 0, 1, 2, 4 }, new
{
FeatureOption.None,
FeatureOption.Enable,
FeatureOption.Warn,
FeatureOption.Telemetry
}.Select(o => o.Flag));
Assert.Equal(FeatureOption.Enable, FeatureOption.Enable);
Assert.NotEqual(FeatureOption.Enable, FeatureOption.Telemetry);
var fromName = FeatureOption.FromName("Warn");
Assert.Equal(FeatureOption.Warn, fromName);
var fromValue = FeatureOption.FromValue(3);
var enableWarn = FeatureOption.Enable | FeatureOption.Warn;
Assert.Equal(enableWarn, fromValue);
var names = $"{enableWarn:names}";
var flags = $"{enableWarn:flags}";
var namesAndFlags = $"{enableWarn:names+flags}";
var @default = $"{enableWarn}";
Assert.True(FeatureOption.None < FeatureOption.Enable);
Assert.True(FeatureOption.Enable < FeatureOption.Telemetry);
Assert.Throws<ArgumentOutOfRangeException>(() => FeatureOption.FromValue(1000));
//Assert.ThrowsAny<DynamicException>(() => FeatureOption.Create("All", 111111));
}
}
The intended usage is:
- Logger layer where the user can define their custom log-levels
- FeatureService where the user can define their custom behaviours
- Other services that work with some default options and let the user customize it with their domain-specific flags.
$endgroup$
$begingroup$
Does a private protected constructor work? I have never seen this before.
$endgroup$
– dfhwze
Jun 16 at 16:03
1
$begingroup$
@dfhwze this is new in7.2
and the first time here where it was actually useful, see C# 7 Series, Part 5: Private Protected This way I can make sure onlyOption<T>
can access it and no other type and guarantee that nobody will break the constraintclass Option<T> : Option where T : Option
$endgroup$
– t3chb0t
Jun 16 at 16:07
add a comment
|
$begingroup$
(self-answer)
v3
I wanted to use v2
of this code (below) to upgrade my old MimeType
that was very similar but it turned out I cannot because I need string
values (like application/json
) and not numerical ones (like 1
) (which are rarely useful anyway) so I've changed the whole thing to work with my SoftString
and replaced binary operations with HashSet
s. Alternatively this could use a generic value but currently I don't see any use for them.
[PublicAPI]
public abstract class Option
{
protected const string Unknown = nameof(Unknown);
public static readonly IImmutableList<SoftString> ReservedNames =
ImmutableList<SoftString>
.Empty
.Add(nameof(Option<Option>.None))
.Add(nameof(Option<Option>.Known));
// Disallow anyone else to use this class.
// This way we can guarantee that it is used only by the Option<T>.
private protected Option() { }
[NotNull]
public abstract SoftString Name { get; }
public abstract IImmutableSet<SoftString> Values { get; }
public abstract bool IsFlag { get; }
}
[PublicAPI]
[DebuggerDisplay(DebuggerDisplayString.DefaultNoQuotes)]
public abstract class Option<T> : Option, IEquatable<Option<T>>, IFormattable where T : Option
{
// Values are what matters for equality.
private static readonly IEqualityComparer<Option<T>> Comparer = EqualityComparerFactory<Option<T>>.Create
(
equals: (left, right) => left.Values.SetEquals(right.Values),
getHashCode: (obj) => obj.Values.GetHashCode()
);
// ReSharper disable once StaticMemberInGenericType - this is correct
private static readonly ConstructorInfo Constructor;
static Option()
{
Constructor =
typeof(T).GetConstructor(new { typeof(SoftString), typeof(IImmutableSet<SoftString>) })
?? throw DynamicException.Create
(
"ConstructorNotFound",
$"{typeof(T).ToPrettyString()} must provide a constructor with the following signature: " +
$"ctor({typeof(SoftString).ToPrettyString()}, {typeof(int).ToPrettyString()})"
);
// Always initialize "None".
var none = New(nameof(None), ImmutableHashSet<SoftString>.Empty.Add(nameof(None)));
Known = ImmutableHashSet<T>.Empty.Add(none);
}
protected Option(SoftString name, IImmutableSet<SoftString> values)
{
Name = name;
Values = values;
}
[NotNull]
public static T None => Known.Single(o => o.Name == nameof(None));
/// <summary>
/// Gets all known options ever created for this type.
/// </summary>
[NotNull]
public static IImmutableSet<T> Known { get; private set; }
/// <summary>
/// Gets options that have only a single value.
/// </summary>
[NotNull, ItemNotNull]
public static IEnumerable<T> Bits => Known.Where(o => o.IsFlag);
#region Option
public override SoftString Name { [DebuggerStepThrough] get; }
public override IImmutableSet<SoftString> Values { get; }
/// <summary>
/// Gets value indicating whether this option has only a single value.
/// </summary>
public override bool IsFlag => Values.Count == 1;
#endregion
#region Factories
public static T Create(SoftString name, params SoftString values)
{
return Create(name, values.ToImmutableHashSet());
}
[NotNull]
public static T Create(SoftString name, IImmutableSet<SoftString> values)
{
if (name.In(ReservedNames))
{
throw DynamicException.Create("ReservedOption", $"The option '{name}' is reserved and must not be created by the user.");
}
if (name.In(Known.Select(o => o.Name)))
{
throw DynamicException.Create("DuplicateOption", $"The option '{name}' is already defined.");
}
var newOption = New(name, values);
if (name == Unknown)
{
return newOption;
}
Known = Known.Add(newOption);
return newOption;
}
private static T New(SoftString name, IImmutableSet<SoftString> values)
{
return (T)Constructor.Invoke(new object
{
name,
values.Any()
? values
: ImmutableHashSet<SoftString>.Empty.Add(name)
});
}
[NotNull]
public static T CreateWithCallerName([CanBeNull] string value = default, [CallerMemberName] string name = default)
{
return Create(name, value ?? name);
}
[NotNull]
public static T FromName([NotNull] string name)
{
if (name == null) throw new ArgumentNullException(nameof(name));
return
Known.FirstOrDefault(o => o.Name == name)
?? throw DynamicException.Create("OptionOutOfRange", $"There is no such option as '{name}'.");
}
private static bool TryGetKnownOption(IEnumerable<SoftString> values, out T option)
{
if (Known.SingleOrDefault(o => o.Values.SetEquals(values)) is var knownOption && !(knownOption is null))
{
option = knownOption;
return true;
}
else
{
option = default;
return false;
}
}
#endregion
public T Set(Option<T> option) => this | option;
public T Reset(Option<T> option) => this ^ option;
[DebuggerStepThrough]
public string ToString(string format, IFormatProvider formatProvider)
{
if (format.In(new { "asc", null }, SoftString.Comparer))
{
return Values.OrderBy(x => x).Select(x => $"{x.ToString()}").Join(", ");
}
if (format.In(new { "desc" }, SoftString.Comparer))
{
return Values.OrderByDescending(x => x).Select(x => $"{x.ToString()}").Join(", ");
}
return ToString();
}
public override string ToString() => $"{this:asc}";
public bool Contains(T option) => Values.Overlaps(option.Values);
#region IEquatable
public bool Equals(Option<T> other) => Comparer.Equals(this, other);
public override bool Equals(object obj) => Equals(obj as Option<T>);
public override int GetHashCode() => Comparer.GetHashCode(this);
#endregion
#region Operators
public static implicit operator string(Option<T> option) => option?.ToString() ?? throw new ArgumentNullException(nameof(option));
public static bool operator ==(Option<T> left, Option<T> right) => Comparer.Equals(left, right);
public static bool operator !=(Option<T> left, Option<T> right) => !(left == right);
[NotNull]
public static T operator |(Option<T> left, Option<T> right)
{
var values = left.Values.Concat(right.Values).ToImmutableHashSet();
return GetKnownOrCreate(values);
}
[NotNull]
public static T operator ^(Option<T> left, Option<T> right)
{
var values = left.Values.Except(right.Values).ToImmutableHashSet();
return GetKnownOrCreate(values);
}
private static T GetKnownOrCreate(IImmutableSet<SoftString> values)
{
return
TryGetKnownOption(values, out var knownOption)
? knownOption
: Create(Unknown, values);
}
#endregion
}
v2
I have made a couple of changes so here's the summary and the improved code:
- Using
CallerMemberName
for automatic option names, however, it's still possible to create custom options with anyname. - Using generic
Option<T>
to remove theDictionary
and provide a few default properties such asNone
,All
orMax
,Bits
. - Cleaned-up naming; now parsing APIs are called
FromName
andFromValue
- Added internal set of options so that I can check whether an option is already defined and use it for other properties like
All
,Max
andBits
. - Added multi-bit support.
- Not using
BitVector32
yet... maybe later. - Added
IFormattable
interface and three formats:names
,flags
andnames+flags
. - Encapsulated operators
|
and^
respectively asSet
andReset
. - Added
Flags
property that enumerates all bits of an option.
[PublicAPI]
public abstract class Option
{
public static readonly IImmutableList<SoftString> ReservedNames =
ImmutableList<SoftString>
.Empty
.Add(nameof(Option<Option>.None))
.Add(nameof(Option<Option>.All))
.Add(nameof(Option<Option>.Max));
// Disallow anyone else to use this class.
// This way we can guarantee that it is used only by the Option<T>.
private protected Option() { }
[NotNull]
public abstract SoftString Name { get; }
public abstract int Flag { get; }
/// <summary>
/// Returns True if Option is power of two.
/// </summary>
public abstract bool IsBit { get; }
}
[PublicAPI]
[DebuggerDisplay(DebuggerDisplayString.DefaultNoQuotes)]
public abstract class Option<T> : Option, IEquatable<Option<T>>, IComparable<Option<T>>, IComparable, IFormattable where T : Option
{
protected const string Unknown = nameof(Unknown);
private static readonly OptionComparer Comparer = new OptionComparer();
private static IImmutableSet<T> Options;
static Option()
{
// Always initialize "None".
Options = ImmutableSortedSet<T>.Empty.Add(Create(nameof(None), 0));
}
protected Option(SoftString name, int flag)
{
if (GetType() != typeof(T)) throw DynamicException.Create("OptionTypeMismatch", "Option must be a type of itself.");
Name = name;
Flag = flag;
}
#region Default options
[NotNull]
public static T None => Options.First();
[NotNull]
public static T Max => Options.Last();
[NotNull]
public static IEnumerable<T> All => Options;
#endregion
[NotNull, ItemNotNull]
public static IEnumerable<T> Bits => Options.Where(o => o.IsBit);
#region Option
public override SoftString Name { [DebuggerStepThrough] get; }
[AutoEqualityProperty]
public override int Flag { [DebuggerStepThrough] get; }
public override bool IsBit => (Flag & (Flag - 1)) == 0;
#endregion
[NotNull, ItemNotNull]
public IEnumerable<T> Flags => Bits.Where(f => (Flag & f.Flag) > 0);
#region Factories
[NotNull]
public static T Create(SoftString name, T option = default)
{
if (name.In(Options.Select(o => o.Name).Concat(ReservedNames)))
{
throw DynamicException.Create("DuplicateOption", $"The option '{name}' is defined more the once.");
}
var bitCount = Options.Count(o => o.IsBit);
var newOption = Create(name, bitCount == 1 ? 1 : (bitCount - 1) << 1);
Options = Options.Add(newOption);
return newOption;
}
[NotNull]
public static T CreateWithCallerName(T option = default, [CallerMemberName] string name = default)
{
return Create(name, option);
}
private static T Create(SoftString name, IEnumerable<int> flags)
{
var flag = flags.Aggregate(0, (current, next) => current | next);
return (T)Activator.CreateInstance(typeof(T), name, flag);
}
public static T Create(SoftString name, params int flags)
{
return Create(name, flags.AsEnumerable());
}
[NotNull]
public static T FromName([NotNull] string value)
{
if (value == null) throw new ArgumentNullException(nameof(value));
return
Options.FirstOrDefault(o => o.Name == value)
?? throw DynamicException.Create("OptionOutOfRange", $"There is no such option as '{value}'.");
}
[NotNull]
public static T FromValue(int value)
{
if (value > Max.Flag)
{
throw new ArgumentOutOfRangeException(paramName: nameof(value), $"Value {value} is greater than the highest option.");
}
// Is this a known value?
if (TryGetKnownOption(value, out var knownOption))
{
return knownOption;
}
var newFlags = Bits.Where(o => (o.Flag & value) == o.Flag).Select(o => o.Flag);
return Create(Unknown, newFlags);
}
private static bool TryGetKnownOption(int flag, out T option)
{
if (Options.SingleOrDefault(o => o.Flag == flag) is var knownOption && !(knownOption is null))
{
option = knownOption;
return true;
}
else
{
option = default;
return false;
}
}
#endregion
public T Set(Option<T> option)
{
return this | option;
}
public T Reset(Option<T> option)
{
return this ^ option;
}
[DebuggerStepThrough]
public string ToString(string format, IFormatProvider formatProvider)
{
if (SoftString.Comparer.Equals(format, "names"))
{
return Flags.Select(o => $"{o.Name.ToString()}").Join(", ");
}
if (SoftString.Comparer.Equals(format, "flags"))
{
return Flags.Select(o => $"{o.Flag}").Join(", ");
}
if (SoftString.Comparer.Equals(format, "names+flags"))
{
return Flags.Select(o => $"{o.Name.ToString()}[{o.Flag}]").Join(", ");
}
return ToString();
}
public override string ToString() => $"{this:names}";
public bool Contains(T option) => Contains(option.Flag);
public bool Contains(int flags) => (Flag & flags) == flags;
public int CompareTo(Option<T> other) => Comparer.Compare(this, other);
public int CompareTo(object other) => Comparer.Compare(this, other);
#region IEquatable
public bool Equals(Option<T> other) => AutoEquality<Option<T>>.Comparer.Equals(this, other);
public override bool Equals(object obj) => Equals(obj as Option<T>);
public override int GetHashCode() => AutoEquality<Option<T>>.Comparer.GetHashCode(this);
#endregion
#region Operators
public static implicit operator string(Option<T> option) => option?.ToString() ?? throw new ArgumentNullException(nameof(option));
public static implicit operator int(Option<T> option) => option?.Flag ?? throw new ArgumentNullException(nameof(option));
public static bool operator ==(Option<T> left, Option<T> right) => Comparer.Compare(left, right) == 0;
public static bool operator !=(Option<T> left, Option<T> right) => !(left == right);
public static bool operator <(Option<T> left, Option<T> right) => Comparer.Compare(left, right) < 0;
public static bool operator <=(Option<T> left, Option<T> right) => Comparer.Compare(left, right) <= 0;
public static bool operator >(Option<T> left, Option<T> right) => Comparer.Compare(left, right) > 0;
public static bool operator >=(Option<T> left, Option<T> right) => Comparer.Compare(left, right) >= 0;
[NotNull]
public static T operator |(Option<T> left, Option<T> right) => GetKnownOrCreate(left.Flag | right.Flag);
[NotNull]
public static T operator ^(Option<T> left, Option<T> right) => GetKnownOrCreate(left.Flag ^ right.Flag);
private static T GetKnownOrCreate(int flag)
{
return
TryGetKnownOption(flag, out var knownOption)
? knownOption
: Create(Unknown, flag);
}
#endregion
private class OptionComparer : IComparer<Option<T>>, IComparer
{
public int Compare(Option<T> left, Option<T> right)
{
if (ReferenceEquals(left, right)) return 0;
if (ReferenceEquals(left, null)) return 1;
if (ReferenceEquals(right, null)) return -1;
return left.Flag - right.Flag;
}
public int Compare(object left, object right)
{
return Compare(left as Option<T>, right as Option<T>);
}
}
}
A new option-set can now be defined by deriving it from Option<T>
and adding static
properties for the desired flags:
public class FeatureOption : Option<FeatureOption>
{
public FeatureOption(SoftString name, int value) : base(name, value) { }
/// <summary>
/// When set a feature is enabled.
/// </summary>
public static readonly FeatureOption Enable = CreateWithCallerName();
/// <summary>
/// When set a warning is logged when a feature is toggled.
/// </summary>
public static readonly FeatureOption Warn = CreateWithCallerName();
/// <summary>
/// When set feature usage statistics are logged.
/// </summary>
public static readonly FeatureOption Telemetry = CreateWithCallerName();
public static readonly FeatureOption Default = CreateWithCallerName(Enable | Warn);
}
Since there is only one option-class now, tests have also become simpler.
public class OptionTest
{
[Fact]
public void Examples()
{
Assert.Equal(new { 0, 1, 2, 4 }, new
{
FeatureOption.None,
FeatureOption.Enable,
FeatureOption.Warn,
FeatureOption.Telemetry
}.Select(o => o.Flag));
Assert.Equal(FeatureOption.Enable, FeatureOption.Enable);
Assert.NotEqual(FeatureOption.Enable, FeatureOption.Telemetry);
var fromName = FeatureOption.FromName("Warn");
Assert.Equal(FeatureOption.Warn, fromName);
var fromValue = FeatureOption.FromValue(3);
var enableWarn = FeatureOption.Enable | FeatureOption.Warn;
Assert.Equal(enableWarn, fromValue);
var names = $"{enableWarn:names}";
var flags = $"{enableWarn:flags}";
var namesAndFlags = $"{enableWarn:names+flags}";
var @default = $"{enableWarn}";
Assert.True(FeatureOption.None < FeatureOption.Enable);
Assert.True(FeatureOption.Enable < FeatureOption.Telemetry);
Assert.Throws<ArgumentOutOfRangeException>(() => FeatureOption.FromValue(1000));
//Assert.ThrowsAny<DynamicException>(() => FeatureOption.Create("All", 111111));
}
}
The intended usage is:
- Logger layer where the user can define their custom log-levels
- FeatureService where the user can define their custom behaviours
- Other services that work with some default options and let the user customize it with their domain-specific flags.
$endgroup$
$begingroup$
Does a private protected constructor work? I have never seen this before.
$endgroup$
– dfhwze
Jun 16 at 16:03
1
$begingroup$
@dfhwze this is new in7.2
and the first time here where it was actually useful, see C# 7 Series, Part 5: Private Protected This way I can make sure onlyOption<T>
can access it and no other type and guarantee that nobody will break the constraintclass Option<T> : Option where T : Option
$endgroup$
– t3chb0t
Jun 16 at 16:07
add a comment
|
$begingroup$
(self-answer)
v3
I wanted to use v2
of this code (below) to upgrade my old MimeType
that was very similar but it turned out I cannot because I need string
values (like application/json
) and not numerical ones (like 1
) (which are rarely useful anyway) so I've changed the whole thing to work with my SoftString
and replaced binary operations with HashSet
s. Alternatively this could use a generic value but currently I don't see any use for them.
[PublicAPI]
public abstract class Option
{
protected const string Unknown = nameof(Unknown);
public static readonly IImmutableList<SoftString> ReservedNames =
ImmutableList<SoftString>
.Empty
.Add(nameof(Option<Option>.None))
.Add(nameof(Option<Option>.Known));
// Disallow anyone else to use this class.
// This way we can guarantee that it is used only by the Option<T>.
private protected Option() { }
[NotNull]
public abstract SoftString Name { get; }
public abstract IImmutableSet<SoftString> Values { get; }
public abstract bool IsFlag { get; }
}
[PublicAPI]
[DebuggerDisplay(DebuggerDisplayString.DefaultNoQuotes)]
public abstract class Option<T> : Option, IEquatable<Option<T>>, IFormattable where T : Option
{
// Values are what matters for equality.
private static readonly IEqualityComparer<Option<T>> Comparer = EqualityComparerFactory<Option<T>>.Create
(
equals: (left, right) => left.Values.SetEquals(right.Values),
getHashCode: (obj) => obj.Values.GetHashCode()
);
// ReSharper disable once StaticMemberInGenericType - this is correct
private static readonly ConstructorInfo Constructor;
static Option()
{
Constructor =
typeof(T).GetConstructor(new { typeof(SoftString), typeof(IImmutableSet<SoftString>) })
?? throw DynamicException.Create
(
"ConstructorNotFound",
$"{typeof(T).ToPrettyString()} must provide a constructor with the following signature: " +
$"ctor({typeof(SoftString).ToPrettyString()}, {typeof(int).ToPrettyString()})"
);
// Always initialize "None".
var none = New(nameof(None), ImmutableHashSet<SoftString>.Empty.Add(nameof(None)));
Known = ImmutableHashSet<T>.Empty.Add(none);
}
protected Option(SoftString name, IImmutableSet<SoftString> values)
{
Name = name;
Values = values;
}
[NotNull]
public static T None => Known.Single(o => o.Name == nameof(None));
/// <summary>
/// Gets all known options ever created for this type.
/// </summary>
[NotNull]
public static IImmutableSet<T> Known { get; private set; }
/// <summary>
/// Gets options that have only a single value.
/// </summary>
[NotNull, ItemNotNull]
public static IEnumerable<T> Bits => Known.Where(o => o.IsFlag);
#region Option
public override SoftString Name { [DebuggerStepThrough] get; }
public override IImmutableSet<SoftString> Values { get; }
/// <summary>
/// Gets value indicating whether this option has only a single value.
/// </summary>
public override bool IsFlag => Values.Count == 1;
#endregion
#region Factories
public static T Create(SoftString name, params SoftString values)
{
return Create(name, values.ToImmutableHashSet());
}
[NotNull]
public static T Create(SoftString name, IImmutableSet<SoftString> values)
{
if (name.In(ReservedNames))
{
throw DynamicException.Create("ReservedOption", $"The option '{name}' is reserved and must not be created by the user.");
}
if (name.In(Known.Select(o => o.Name)))
{
throw DynamicException.Create("DuplicateOption", $"The option '{name}' is already defined.");
}
var newOption = New(name, values);
if (name == Unknown)
{
return newOption;
}
Known = Known.Add(newOption);
return newOption;
}
private static T New(SoftString name, IImmutableSet<SoftString> values)
{
return (T)Constructor.Invoke(new object
{
name,
values.Any()
? values
: ImmutableHashSet<SoftString>.Empty.Add(name)
});
}
[NotNull]
public static T CreateWithCallerName([CanBeNull] string value = default, [CallerMemberName] string name = default)
{
return Create(name, value ?? name);
}
[NotNull]
public static T FromName([NotNull] string name)
{
if (name == null) throw new ArgumentNullException(nameof(name));
return
Known.FirstOrDefault(o => o.Name == name)
?? throw DynamicException.Create("OptionOutOfRange", $"There is no such option as '{name}'.");
}
private static bool TryGetKnownOption(IEnumerable<SoftString> values, out T option)
{
if (Known.SingleOrDefault(o => o.Values.SetEquals(values)) is var knownOption && !(knownOption is null))
{
option = knownOption;
return true;
}
else
{
option = default;
return false;
}
}
#endregion
public T Set(Option<T> option) => this | option;
public T Reset(Option<T> option) => this ^ option;
[DebuggerStepThrough]
public string ToString(string format, IFormatProvider formatProvider)
{
if (format.In(new { "asc", null }, SoftString.Comparer))
{
return Values.OrderBy(x => x).Select(x => $"{x.ToString()}").Join(", ");
}
if (format.In(new { "desc" }, SoftString.Comparer))
{
return Values.OrderByDescending(x => x).Select(x => $"{x.ToString()}").Join(", ");
}
return ToString();
}
public override string ToString() => $"{this:asc}";
public bool Contains(T option) => Values.Overlaps(option.Values);
#region IEquatable
public bool Equals(Option<T> other) => Comparer.Equals(this, other);
public override bool Equals(object obj) => Equals(obj as Option<T>);
public override int GetHashCode() => Comparer.GetHashCode(this);
#endregion
#region Operators
public static implicit operator string(Option<T> option) => option?.ToString() ?? throw new ArgumentNullException(nameof(option));
public static bool operator ==(Option<T> left, Option<T> right) => Comparer.Equals(left, right);
public static bool operator !=(Option<T> left, Option<T> right) => !(left == right);
[NotNull]
public static T operator |(Option<T> left, Option<T> right)
{
var values = left.Values.Concat(right.Values).ToImmutableHashSet();
return GetKnownOrCreate(values);
}
[NotNull]
public static T operator ^(Option<T> left, Option<T> right)
{
var values = left.Values.Except(right.Values).ToImmutableHashSet();
return GetKnownOrCreate(values);
}
private static T GetKnownOrCreate(IImmutableSet<SoftString> values)
{
return
TryGetKnownOption(values, out var knownOption)
? knownOption
: Create(Unknown, values);
}
#endregion
}
v2
I have made a couple of changes so here's the summary and the improved code:
- Using
CallerMemberName
for automatic option names, however, it's still possible to create custom options with anyname. - Using generic
Option<T>
to remove theDictionary
and provide a few default properties such asNone
,All
orMax
,Bits
. - Cleaned-up naming; now parsing APIs are called
FromName
andFromValue
- Added internal set of options so that I can check whether an option is already defined and use it for other properties like
All
,Max
andBits
. - Added multi-bit support.
- Not using
BitVector32
yet... maybe later. - Added
IFormattable
interface and three formats:names
,flags
andnames+flags
. - Encapsulated operators
|
and^
respectively asSet
andReset
. - Added
Flags
property that enumerates all bits of an option.
[PublicAPI]
public abstract class Option
{
public static readonly IImmutableList<SoftString> ReservedNames =
ImmutableList<SoftString>
.Empty
.Add(nameof(Option<Option>.None))
.Add(nameof(Option<Option>.All))
.Add(nameof(Option<Option>.Max));
// Disallow anyone else to use this class.
// This way we can guarantee that it is used only by the Option<T>.
private protected Option() { }
[NotNull]
public abstract SoftString Name { get; }
public abstract int Flag { get; }
/// <summary>
/// Returns True if Option is power of two.
/// </summary>
public abstract bool IsBit { get; }
}
[PublicAPI]
[DebuggerDisplay(DebuggerDisplayString.DefaultNoQuotes)]
public abstract class Option<T> : Option, IEquatable<Option<T>>, IComparable<Option<T>>, IComparable, IFormattable where T : Option
{
protected const string Unknown = nameof(Unknown);
private static readonly OptionComparer Comparer = new OptionComparer();
private static IImmutableSet<T> Options;
static Option()
{
// Always initialize "None".
Options = ImmutableSortedSet<T>.Empty.Add(Create(nameof(None), 0));
}
protected Option(SoftString name, int flag)
{
if (GetType() != typeof(T)) throw DynamicException.Create("OptionTypeMismatch", "Option must be a type of itself.");
Name = name;
Flag = flag;
}
#region Default options
[NotNull]
public static T None => Options.First();
[NotNull]
public static T Max => Options.Last();
[NotNull]
public static IEnumerable<T> All => Options;
#endregion
[NotNull, ItemNotNull]
public static IEnumerable<T> Bits => Options.Where(o => o.IsBit);
#region Option
public override SoftString Name { [DebuggerStepThrough] get; }
[AutoEqualityProperty]
public override int Flag { [DebuggerStepThrough] get; }
public override bool IsBit => (Flag & (Flag - 1)) == 0;
#endregion
[NotNull, ItemNotNull]
public IEnumerable<T> Flags => Bits.Where(f => (Flag & f.Flag) > 0);
#region Factories
[NotNull]
public static T Create(SoftString name, T option = default)
{
if (name.In(Options.Select(o => o.Name).Concat(ReservedNames)))
{
throw DynamicException.Create("DuplicateOption", $"The option '{name}' is defined more the once.");
}
var bitCount = Options.Count(o => o.IsBit);
var newOption = Create(name, bitCount == 1 ? 1 : (bitCount - 1) << 1);
Options = Options.Add(newOption);
return newOption;
}
[NotNull]
public static T CreateWithCallerName(T option = default, [CallerMemberName] string name = default)
{
return Create(name, option);
}
private static T Create(SoftString name, IEnumerable<int> flags)
{
var flag = flags.Aggregate(0, (current, next) => current | next);
return (T)Activator.CreateInstance(typeof(T), name, flag);
}
public static T Create(SoftString name, params int flags)
{
return Create(name, flags.AsEnumerable());
}
[NotNull]
public static T FromName([NotNull] string value)
{
if (value == null) throw new ArgumentNullException(nameof(value));
return
Options.FirstOrDefault(o => o.Name == value)
?? throw DynamicException.Create("OptionOutOfRange", $"There is no such option as '{value}'.");
}
[NotNull]
public static T FromValue(int value)
{
if (value > Max.Flag)
{
throw new ArgumentOutOfRangeException(paramName: nameof(value), $"Value {value} is greater than the highest option.");
}
// Is this a known value?
if (TryGetKnownOption(value, out var knownOption))
{
return knownOption;
}
var newFlags = Bits.Where(o => (o.Flag & value) == o.Flag).Select(o => o.Flag);
return Create(Unknown, newFlags);
}
private static bool TryGetKnownOption(int flag, out T option)
{
if (Options.SingleOrDefault(o => o.Flag == flag) is var knownOption && !(knownOption is null))
{
option = knownOption;
return true;
}
else
{
option = default;
return false;
}
}
#endregion
public T Set(Option<T> option)
{
return this | option;
}
public T Reset(Option<T> option)
{
return this ^ option;
}
[DebuggerStepThrough]
public string ToString(string format, IFormatProvider formatProvider)
{
if (SoftString.Comparer.Equals(format, "names"))
{
return Flags.Select(o => $"{o.Name.ToString()}").Join(", ");
}
if (SoftString.Comparer.Equals(format, "flags"))
{
return Flags.Select(o => $"{o.Flag}").Join(", ");
}
if (SoftString.Comparer.Equals(format, "names+flags"))
{
return Flags.Select(o => $"{o.Name.ToString()}[{o.Flag}]").Join(", ");
}
return ToString();
}
public override string ToString() => $"{this:names}";
public bool Contains(T option) => Contains(option.Flag);
public bool Contains(int flags) => (Flag & flags) == flags;
public int CompareTo(Option<T> other) => Comparer.Compare(this, other);
public int CompareTo(object other) => Comparer.Compare(this, other);
#region IEquatable
public bool Equals(Option<T> other) => AutoEquality<Option<T>>.Comparer.Equals(this, other);
public override bool Equals(object obj) => Equals(obj as Option<T>);
public override int GetHashCode() => AutoEquality<Option<T>>.Comparer.GetHashCode(this);
#endregion
#region Operators
public static implicit operator string(Option<T> option) => option?.ToString() ?? throw new ArgumentNullException(nameof(option));
public static implicit operator int(Option<T> option) => option?.Flag ?? throw new ArgumentNullException(nameof(option));
public static bool operator ==(Option<T> left, Option<T> right) => Comparer.Compare(left, right) == 0;
public static bool operator !=(Option<T> left, Option<T> right) => !(left == right);
public static bool operator <(Option<T> left, Option<T> right) => Comparer.Compare(left, right) < 0;
public static bool operator <=(Option<T> left, Option<T> right) => Comparer.Compare(left, right) <= 0;
public static bool operator >(Option<T> left, Option<T> right) => Comparer.Compare(left, right) > 0;
public static bool operator >=(Option<T> left, Option<T> right) => Comparer.Compare(left, right) >= 0;
[NotNull]
public static T operator |(Option<T> left, Option<T> right) => GetKnownOrCreate(left.Flag | right.Flag);
[NotNull]
public static T operator ^(Option<T> left, Option<T> right) => GetKnownOrCreate(left.Flag ^ right.Flag);
private static T GetKnownOrCreate(int flag)
{
return
TryGetKnownOption(flag, out var knownOption)
? knownOption
: Create(Unknown, flag);
}
#endregion
private class OptionComparer : IComparer<Option<T>>, IComparer
{
public int Compare(Option<T> left, Option<T> right)
{
if (ReferenceEquals(left, right)) return 0;
if (ReferenceEquals(left, null)) return 1;
if (ReferenceEquals(right, null)) return -1;
return left.Flag - right.Flag;
}
public int Compare(object left, object right)
{
return Compare(left as Option<T>, right as Option<T>);
}
}
}
A new option-set can now be defined by deriving it from Option<T>
and adding static
properties for the desired flags:
public class FeatureOption : Option<FeatureOption>
{
public FeatureOption(SoftString name, int value) : base(name, value) { }
/// <summary>
/// When set a feature is enabled.
/// </summary>
public static readonly FeatureOption Enable = CreateWithCallerName();
/// <summary>
/// When set a warning is logged when a feature is toggled.
/// </summary>
public static readonly FeatureOption Warn = CreateWithCallerName();
/// <summary>
/// When set feature usage statistics are logged.
/// </summary>
public static readonly FeatureOption Telemetry = CreateWithCallerName();
public static readonly FeatureOption Default = CreateWithCallerName(Enable | Warn);
}
Since there is only one option-class now, tests have also become simpler.
public class OptionTest
{
[Fact]
public void Examples()
{
Assert.Equal(new { 0, 1, 2, 4 }, new
{
FeatureOption.None,
FeatureOption.Enable,
FeatureOption.Warn,
FeatureOption.Telemetry
}.Select(o => o.Flag));
Assert.Equal(FeatureOption.Enable, FeatureOption.Enable);
Assert.NotEqual(FeatureOption.Enable, FeatureOption.Telemetry);
var fromName = FeatureOption.FromName("Warn");
Assert.Equal(FeatureOption.Warn, fromName);
var fromValue = FeatureOption.FromValue(3);
var enableWarn = FeatureOption.Enable | FeatureOption.Warn;
Assert.Equal(enableWarn, fromValue);
var names = $"{enableWarn:names}";
var flags = $"{enableWarn:flags}";
var namesAndFlags = $"{enableWarn:names+flags}";
var @default = $"{enableWarn}";
Assert.True(FeatureOption.None < FeatureOption.Enable);
Assert.True(FeatureOption.Enable < FeatureOption.Telemetry);
Assert.Throws<ArgumentOutOfRangeException>(() => FeatureOption.FromValue(1000));
//Assert.ThrowsAny<DynamicException>(() => FeatureOption.Create("All", 111111));
}
}
The intended usage is:
- Logger layer where the user can define their custom log-levels
- FeatureService where the user can define their custom behaviours
- Other services that work with some default options and let the user customize it with their domain-specific flags.
$endgroup$
(self-answer)
v3
I wanted to use v2
of this code (below) to upgrade my old MimeType
that was very similar but it turned out I cannot because I need string
values (like application/json
) and not numerical ones (like 1
) (which are rarely useful anyway) so I've changed the whole thing to work with my SoftString
and replaced binary operations with HashSet
s. Alternatively this could use a generic value but currently I don't see any use for them.
[PublicAPI]
public abstract class Option
{
protected const string Unknown = nameof(Unknown);
public static readonly IImmutableList<SoftString> ReservedNames =
ImmutableList<SoftString>
.Empty
.Add(nameof(Option<Option>.None))
.Add(nameof(Option<Option>.Known));
// Disallow anyone else to use this class.
// This way we can guarantee that it is used only by the Option<T>.
private protected Option() { }
[NotNull]
public abstract SoftString Name { get; }
public abstract IImmutableSet<SoftString> Values { get; }
public abstract bool IsFlag { get; }
}
[PublicAPI]
[DebuggerDisplay(DebuggerDisplayString.DefaultNoQuotes)]
public abstract class Option<T> : Option, IEquatable<Option<T>>, IFormattable where T : Option
{
// Values are what matters for equality.
private static readonly IEqualityComparer<Option<T>> Comparer = EqualityComparerFactory<Option<T>>.Create
(
equals: (left, right) => left.Values.SetEquals(right.Values),
getHashCode: (obj) => obj.Values.GetHashCode()
);
// ReSharper disable once StaticMemberInGenericType - this is correct
private static readonly ConstructorInfo Constructor;
static Option()
{
Constructor =
typeof(T).GetConstructor(new { typeof(SoftString), typeof(IImmutableSet<SoftString>) })
?? throw DynamicException.Create
(
"ConstructorNotFound",
$"{typeof(T).ToPrettyString()} must provide a constructor with the following signature: " +
$"ctor({typeof(SoftString).ToPrettyString()}, {typeof(int).ToPrettyString()})"
);
// Always initialize "None".
var none = New(nameof(None), ImmutableHashSet<SoftString>.Empty.Add(nameof(None)));
Known = ImmutableHashSet<T>.Empty.Add(none);
}
protected Option(SoftString name, IImmutableSet<SoftString> values)
{
Name = name;
Values = values;
}
[NotNull]
public static T None => Known.Single(o => o.Name == nameof(None));
/// <summary>
/// Gets all known options ever created for this type.
/// </summary>
[NotNull]
public static IImmutableSet<T> Known { get; private set; }
/// <summary>
/// Gets options that have only a single value.
/// </summary>
[NotNull, ItemNotNull]
public static IEnumerable<T> Bits => Known.Where(o => o.IsFlag);
#region Option
public override SoftString Name { [DebuggerStepThrough] get; }
public override IImmutableSet<SoftString> Values { get; }
/// <summary>
/// Gets value indicating whether this option has only a single value.
/// </summary>
public override bool IsFlag => Values.Count == 1;
#endregion
#region Factories
public static T Create(SoftString name, params SoftString values)
{
return Create(name, values.ToImmutableHashSet());
}
[NotNull]
public static T Create(SoftString name, IImmutableSet<SoftString> values)
{
if (name.In(ReservedNames))
{
throw DynamicException.Create("ReservedOption", $"The option '{name}' is reserved and must not be created by the user.");
}
if (name.In(Known.Select(o => o.Name)))
{
throw DynamicException.Create("DuplicateOption", $"The option '{name}' is already defined.");
}
var newOption = New(name, values);
if (name == Unknown)
{
return newOption;
}
Known = Known.Add(newOption);
return newOption;
}
private static T New(SoftString name, IImmutableSet<SoftString> values)
{
return (T)Constructor.Invoke(new object
{
name,
values.Any()
? values
: ImmutableHashSet<SoftString>.Empty.Add(name)
});
}
[NotNull]
public static T CreateWithCallerName([CanBeNull] string value = default, [CallerMemberName] string name = default)
{
return Create(name, value ?? name);
}
[NotNull]
public static T FromName([NotNull] string name)
{
if (name == null) throw new ArgumentNullException(nameof(name));
return
Known.FirstOrDefault(o => o.Name == name)
?? throw DynamicException.Create("OptionOutOfRange", $"There is no such option as '{name}'.");
}
private static bool TryGetKnownOption(IEnumerable<SoftString> values, out T option)
{
if (Known.SingleOrDefault(o => o.Values.SetEquals(values)) is var knownOption && !(knownOption is null))
{
option = knownOption;
return true;
}
else
{
option = default;
return false;
}
}
#endregion
public T Set(Option<T> option) => this | option;
public T Reset(Option<T> option) => this ^ option;
[DebuggerStepThrough]
public string ToString(string format, IFormatProvider formatProvider)
{
if (format.In(new { "asc", null }, SoftString.Comparer))
{
return Values.OrderBy(x => x).Select(x => $"{x.ToString()}").Join(", ");
}
if (format.In(new { "desc" }, SoftString.Comparer))
{
return Values.OrderByDescending(x => x).Select(x => $"{x.ToString()}").Join(", ");
}
return ToString();
}
public override string ToString() => $"{this:asc}";
public bool Contains(T option) => Values.Overlaps(option.Values);
#region IEquatable
public bool Equals(Option<T> other) => Comparer.Equals(this, other);
public override bool Equals(object obj) => Equals(obj as Option<T>);
public override int GetHashCode() => Comparer.GetHashCode(this);
#endregion
#region Operators
public static implicit operator string(Option<T> option) => option?.ToString() ?? throw new ArgumentNullException(nameof(option));
public static bool operator ==(Option<T> left, Option<T> right) => Comparer.Equals(left, right);
public static bool operator !=(Option<T> left, Option<T> right) => !(left == right);
[NotNull]
public static T operator |(Option<T> left, Option<T> right)
{
var values = left.Values.Concat(right.Values).ToImmutableHashSet();
return GetKnownOrCreate(values);
}
[NotNull]
public static T operator ^(Option<T> left, Option<T> right)
{
var values = left.Values.Except(right.Values).ToImmutableHashSet();
return GetKnownOrCreate(values);
}
private static T GetKnownOrCreate(IImmutableSet<SoftString> values)
{
return
TryGetKnownOption(values, out var knownOption)
? knownOption
: Create(Unknown, values);
}
#endregion
}
v2
I have made a couple of changes so here's the summary and the improved code:
- Using
CallerMemberName
for automatic option names, however, it's still possible to create custom options with anyname. - Using generic
Option<T>
to remove theDictionary
and provide a few default properties such asNone
,All
orMax
,Bits
. - Cleaned-up naming; now parsing APIs are called
FromName
andFromValue
- Added internal set of options so that I can check whether an option is already defined and use it for other properties like
All
,Max
andBits
. - Added multi-bit support.
- Not using
BitVector32
yet... maybe later. - Added
IFormattable
interface and three formats:names
,flags
andnames+flags
. - Encapsulated operators
|
and^
respectively asSet
andReset
. - Added
Flags
property that enumerates all bits of an option.
[PublicAPI]
public abstract class Option
{
public static readonly IImmutableList<SoftString> ReservedNames =
ImmutableList<SoftString>
.Empty
.Add(nameof(Option<Option>.None))
.Add(nameof(Option<Option>.All))
.Add(nameof(Option<Option>.Max));
// Disallow anyone else to use this class.
// This way we can guarantee that it is used only by the Option<T>.
private protected Option() { }
[NotNull]
public abstract SoftString Name { get; }
public abstract int Flag { get; }
/// <summary>
/// Returns True if Option is power of two.
/// </summary>
public abstract bool IsBit { get; }
}
[PublicAPI]
[DebuggerDisplay(DebuggerDisplayString.DefaultNoQuotes)]
public abstract class Option<T> : Option, IEquatable<Option<T>>, IComparable<Option<T>>, IComparable, IFormattable where T : Option
{
protected const string Unknown = nameof(Unknown);
private static readonly OptionComparer Comparer = new OptionComparer();
private static IImmutableSet<T> Options;
static Option()
{
// Always initialize "None".
Options = ImmutableSortedSet<T>.Empty.Add(Create(nameof(None), 0));
}
protected Option(SoftString name, int flag)
{
if (GetType() != typeof(T)) throw DynamicException.Create("OptionTypeMismatch", "Option must be a type of itself.");
Name = name;
Flag = flag;
}
#region Default options
[NotNull]
public static T None => Options.First();
[NotNull]
public static T Max => Options.Last();
[NotNull]
public static IEnumerable<T> All => Options;
#endregion
[NotNull, ItemNotNull]
public static IEnumerable<T> Bits => Options.Where(o => o.IsBit);
#region Option
public override SoftString Name { [DebuggerStepThrough] get; }
[AutoEqualityProperty]
public override int Flag { [DebuggerStepThrough] get; }
public override bool IsBit => (Flag & (Flag - 1)) == 0;
#endregion
[NotNull, ItemNotNull]
public IEnumerable<T> Flags => Bits.Where(f => (Flag & f.Flag) > 0);
#region Factories
[NotNull]
public static T Create(SoftString name, T option = default)
{
if (name.In(Options.Select(o => o.Name).Concat(ReservedNames)))
{
throw DynamicException.Create("DuplicateOption", $"The option '{name}' is defined more the once.");
}
var bitCount = Options.Count(o => o.IsBit);
var newOption = Create(name, bitCount == 1 ? 1 : (bitCount - 1) << 1);
Options = Options.Add(newOption);
return newOption;
}
[NotNull]
public static T CreateWithCallerName(T option = default, [CallerMemberName] string name = default)
{
return Create(name, option);
}
private static T Create(SoftString name, IEnumerable<int> flags)
{
var flag = flags.Aggregate(0, (current, next) => current | next);
return (T)Activator.CreateInstance(typeof(T), name, flag);
}
public static T Create(SoftString name, params int flags)
{
return Create(name, flags.AsEnumerable());
}
[NotNull]
public static T FromName([NotNull] string value)
{
if (value == null) throw new ArgumentNullException(nameof(value));
return
Options.FirstOrDefault(o => o.Name == value)
?? throw DynamicException.Create("OptionOutOfRange", $"There is no such option as '{value}'.");
}
[NotNull]
public static T FromValue(int value)
{
if (value > Max.Flag)
{
throw new ArgumentOutOfRangeException(paramName: nameof(value), $"Value {value} is greater than the highest option.");
}
// Is this a known value?
if (TryGetKnownOption(value, out var knownOption))
{
return knownOption;
}
var newFlags = Bits.Where(o => (o.Flag & value) == o.Flag).Select(o => o.Flag);
return Create(Unknown, newFlags);
}
private static bool TryGetKnownOption(int flag, out T option)
{
if (Options.SingleOrDefault(o => o.Flag == flag) is var knownOption && !(knownOption is null))
{
option = knownOption;
return true;
}
else
{
option = default;
return false;
}
}
#endregion
public T Set(Option<T> option)
{
return this | option;
}
public T Reset(Option<T> option)
{
return this ^ option;
}
[DebuggerStepThrough]
public string ToString(string format, IFormatProvider formatProvider)
{
if (SoftString.Comparer.Equals(format, "names"))
{
return Flags.Select(o => $"{o.Name.ToString()}").Join(", ");
}
if (SoftString.Comparer.Equals(format, "flags"))
{
return Flags.Select(o => $"{o.Flag}").Join(", ");
}
if (SoftString.Comparer.Equals(format, "names+flags"))
{
return Flags.Select(o => $"{o.Name.ToString()}[{o.Flag}]").Join(", ");
}
return ToString();
}
public override string ToString() => $"{this:names}";
public bool Contains(T option) => Contains(option.Flag);
public bool Contains(int flags) => (Flag & flags) == flags;
public int CompareTo(Option<T> other) => Comparer.Compare(this, other);
public int CompareTo(object other) => Comparer.Compare(this, other);
#region IEquatable
public bool Equals(Option<T> other) => AutoEquality<Option<T>>.Comparer.Equals(this, other);
public override bool Equals(object obj) => Equals(obj as Option<T>);
public override int GetHashCode() => AutoEquality<Option<T>>.Comparer.GetHashCode(this);
#endregion
#region Operators
public static implicit operator string(Option<T> option) => option?.ToString() ?? throw new ArgumentNullException(nameof(option));
public static implicit operator int(Option<T> option) => option?.Flag ?? throw new ArgumentNullException(nameof(option));
public static bool operator ==(Option<T> left, Option<T> right) => Comparer.Compare(left, right) == 0;
public static bool operator !=(Option<T> left, Option<T> right) => !(left == right);
public static bool operator <(Option<T> left, Option<T> right) => Comparer.Compare(left, right) < 0;
public static bool operator <=(Option<T> left, Option<T> right) => Comparer.Compare(left, right) <= 0;
public static bool operator >(Option<T> left, Option<T> right) => Comparer.Compare(left, right) > 0;
public static bool operator >=(Option<T> left, Option<T> right) => Comparer.Compare(left, right) >= 0;
[NotNull]
public static T operator |(Option<T> left, Option<T> right) => GetKnownOrCreate(left.Flag | right.Flag);
[NotNull]
public static T operator ^(Option<T> left, Option<T> right) => GetKnownOrCreate(left.Flag ^ right.Flag);
private static T GetKnownOrCreate(int flag)
{
return
TryGetKnownOption(flag, out var knownOption)
? knownOption
: Create(Unknown, flag);
}
#endregion
private class OptionComparer : IComparer<Option<T>>, IComparer
{
public int Compare(Option<T> left, Option<T> right)
{
if (ReferenceEquals(left, right)) return 0;
if (ReferenceEquals(left, null)) return 1;
if (ReferenceEquals(right, null)) return -1;
return left.Flag - right.Flag;
}
public int Compare(object left, object right)
{
return Compare(left as Option<T>, right as Option<T>);
}
}
}
A new option-set can now be defined by deriving it from Option<T>
and adding static
properties for the desired flags:
public class FeatureOption : Option<FeatureOption>
{
public FeatureOption(SoftString name, int value) : base(name, value) { }
/// <summary>
/// When set a feature is enabled.
/// </summary>
public static readonly FeatureOption Enable = CreateWithCallerName();
/// <summary>
/// When set a warning is logged when a feature is toggled.
/// </summary>
public static readonly FeatureOption Warn = CreateWithCallerName();
/// <summary>
/// When set feature usage statistics are logged.
/// </summary>
public static readonly FeatureOption Telemetry = CreateWithCallerName();
public static readonly FeatureOption Default = CreateWithCallerName(Enable | Warn);
}
Since there is only one option-class now, tests have also become simpler.
public class OptionTest
{
[Fact]
public void Examples()
{
Assert.Equal(new { 0, 1, 2, 4 }, new
{
FeatureOption.None,
FeatureOption.Enable,
FeatureOption.Warn,
FeatureOption.Telemetry
}.Select(o => o.Flag));
Assert.Equal(FeatureOption.Enable, FeatureOption.Enable);
Assert.NotEqual(FeatureOption.Enable, FeatureOption.Telemetry);
var fromName = FeatureOption.FromName("Warn");
Assert.Equal(FeatureOption.Warn, fromName);
var fromValue = FeatureOption.FromValue(3);
var enableWarn = FeatureOption.Enable | FeatureOption.Warn;
Assert.Equal(enableWarn, fromValue);
var names = $"{enableWarn:names}";
var flags = $"{enableWarn:flags}";
var namesAndFlags = $"{enableWarn:names+flags}";
var @default = $"{enableWarn}";
Assert.True(FeatureOption.None < FeatureOption.Enable);
Assert.True(FeatureOption.Enable < FeatureOption.Telemetry);
Assert.Throws<ArgumentOutOfRangeException>(() => FeatureOption.FromValue(1000));
//Assert.ThrowsAny<DynamicException>(() => FeatureOption.Create("All", 111111));
}
}
The intended usage is:
- Logger layer where the user can define their custom log-levels
- FeatureService where the user can define their custom behaviours
- Other services that work with some default options and let the user customize it with their domain-specific flags.
edited Jun 16 at 15:55
answered May 27 at 17:18
t3chb0tt3chb0t
38.6k7 gold badges62 silver badges142 bronze badges
38.6k7 gold badges62 silver badges142 bronze badges
$begingroup$
Does a private protected constructor work? I have never seen this before.
$endgroup$
– dfhwze
Jun 16 at 16:03
1
$begingroup$
@dfhwze this is new in7.2
and the first time here where it was actually useful, see C# 7 Series, Part 5: Private Protected This way I can make sure onlyOption<T>
can access it and no other type and guarantee that nobody will break the constraintclass Option<T> : Option where T : Option
$endgroup$
– t3chb0t
Jun 16 at 16:07
add a comment
|
$begingroup$
Does a private protected constructor work? I have never seen this before.
$endgroup$
– dfhwze
Jun 16 at 16:03
1
$begingroup$
@dfhwze this is new in7.2
and the first time here where it was actually useful, see C# 7 Series, Part 5: Private Protected This way I can make sure onlyOption<T>
can access it and no other type and guarantee that nobody will break the constraintclass Option<T> : Option where T : Option
$endgroup$
– t3chb0t
Jun 16 at 16:07
$begingroup$
Does a private protected constructor work? I have never seen this before.
$endgroup$
– dfhwze
Jun 16 at 16:03
$begingroup$
Does a private protected constructor work? I have never seen this before.
$endgroup$
– dfhwze
Jun 16 at 16:03
1
1
$begingroup$
@dfhwze this is new in
7.2
and the first time here where it was actually useful, see C# 7 Series, Part 5: Private Protected This way I can make sure only Option<T>
can access it and no other type and guarantee that nobody will break the constraint class Option<T> : Option where T : Option
$endgroup$
– t3chb0t
Jun 16 at 16:07
$begingroup$
@dfhwze this is new in
7.2
and the first time here where it was actually useful, see C# 7 Series, Part 5: Private Protected This way I can make sure only Option<T>
can access it and no other type and guarantee that nobody will break the constraint class Option<T> : Option where T : Option
$endgroup$
– t3chb0t
Jun 16 at 16:07
add a comment
|
Thanks for contributing an answer to Code Review Stack Exchange!
- Please be sure to answer the question. Provide details and share your research!
But avoid …
- Asking for help, clarification, or responding to other answers.
- Making statements based on opinion; back them up with references or personal experience.
Use MathJax to format equations. MathJax reference.
To learn more, see our tips on writing great answers.
Sign up or log in
StackExchange.ready(function () {
StackExchange.helpers.onClickDraftSave('#login-link');
});
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
StackExchange.ready(
function () {
StackExchange.openid.initPostLogin('.new-post-login', 'https%3a%2f%2fcodereview.stackexchange.com%2fquestions%2f221057%2fgeneral-purpose-replacement-for-enum-with-flagsattribute%23new-answer', 'question_page');
}
);
Post as a guest
Required, but never shown
Sign up or log in
StackExchange.ready(function () {
StackExchange.helpers.onClickDraftSave('#login-link');
});
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
Sign up or log in
StackExchange.ready(function () {
StackExchange.helpers.onClickDraftSave('#login-link');
});
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
Sign up or log in
StackExchange.ready(function () {
StackExchange.helpers.onClickDraftSave('#login-link');
});
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
$begingroup$
The current commit.
$endgroup$
– t3chb0t
May 26 at 12:06
$begingroup$
Comments are not for extended discussion; this conversation has been moved to chat.
$endgroup$
– rolfl♦
May 27 at 13:15
$begingroup$
wow, I got another downvote... how so?
$endgroup$
– t3chb0t
Jul 12 at 11:50