如何以交叉兼容的方式"extend"枚举?

  • 本文关键字:方式 extend 枚举 c# .net
  • 更新时间 :
  • 英文 :


目前这更多的是一个假设性问题,但我觉得在某些情况下它可能有用,所以请耐心等待。

假设您正在使用一个处理图像的库。库使用的类型如下所示:

public class Image
{
public virtual ImageFormat Format { get; }
// ...
}
public enum ImageFormat
{
Unknown,
Png,
Gif,
Jpeg
}

这很简单,没有什么太复杂的,但如果在使用库的某个时刻,您实际上添加了对另一种图像格式的支持,但仍然希望使代码与库兼容,该怎么办?当然,只为你暴露这些信息并不是什么大问题:

public class ExtendedImage : Image
{
public override ImageFormat Format => ImageFormat.Unknown;
public ExtendedImageFormat ExtendedFormat { get; }
}
public enum ExtendedImageFormat
{
Bmp,
Tiff,
Webp
}

问题是,如果有更多的库都使用这种方法,那么最终会出现大量相互不兼容的格式,这些格式在外部仅为ImageFormat.Unknown

当然,真正的问题在于ImageImageFormat的定义,您可以认为从枚举切换到类或接口可以正确地解决这个问题。但是,假设您不能控制库,并且不能修改它,因为其他库依赖它(它甚至可能是BCL)。

为了让事情变得更加困难,假设您必须将扩展格式实际存储在旧的ImageFormat中,出于某种原因,根据需要在枚举类型之间进行强制转换。

public enum ExtendedImageFormat // : ImageFormat
{
Bmp = 100001,
Tiff = 100002,
Webp = 100003
}

总之,我正在考虑的是一个具有以下特性的解决方案:

  • 给定一个ImageFormat值,它应该能够从枚举值中检索通常需要检索的内容:它的名称和自定义属性。它不必通过ToString和反射(无论如何都不可能)
  • 给定一个字符串名称,它应该能够解析它并返回ImageFormat
  • 这些功能应该可以从任何其他模块/库中使用,并且一个模块的更改应该在另一个模块中可见
  • 模块是";通信";这种方式本身不需要知道(参考)
  • 新枚举值是";注册的";应该是万无一失的:注册重复的名称/值应该是可检测的
  • 该解决方案不应该具有任何感觉"是"的方面;"任意";,即自定义名称、类型、隐藏在某些资源中的信息等。最好它应该直截了当,并且是经过思考后可以得出的
  • 我想.NET中没有任何东西可以解决这个精确的问题,所以它可以只是一系列的步骤(实际上是几个方法)。最理想的情况是,它应该限制在.NET Standard 2.0中

我在这里考虑像TypeDescriptor这样的类,它允许您向现有类型和对象添加成员。我很清楚;假的";在某种程度上,因为它实际上不会覆盖现有的方法,也不会影响反思,但这是我正在寻找的那种标准化解决方案,只要每个人都在使用它,它就会为他们工作。因此,指向一个单独处理所有这些注册的库可能会很有用,但前提是没有更好的解决方案只使用基本.NET库。

花了一段时间,但我认为这段代码解决了问题:

internal class DerivedEnumProvider<TDerived, TBase> : TypeDescriptionProvider
where TDerived : struct, Enum
where TBase : struct, Enum
{
static readonly Type derivedType = typeof(TDerived);
static readonly Type baseType = typeof(TBase);
static readonly Type underlyingType;
static readonly ulong typeHash;
static DerivedEnumProvider()
{
underlyingType = Enum.GetUnderlyingType(derivedType);
if(!underlyingType.Equals(Enum.GetUnderlyingType(baseType)))
{
throw new NotSupportedException($"{nameof(TDerived)} ({derivedType}) and {nameof(TBase)} ({baseType}) must have the same underlying type.");
}
typeHash = unchecked((uint)derivedType.GetHashCode());
}
readonly TypeConverter baseConverter;
readonly List<TBase> valuesList = new();
readonly TypeConverter.StandardValuesCollection valuesCollection;
readonly Dictionary<TBase, object> derivedMap = new();
readonly Dictionary<TDerived, object> baseMap = new();
public DerivedEnumProvider(TypeDescriptionProvider parent, TypeConverter converter, ICollection standardValues) : base(parent)
{
baseConverter = converter;
var valuesSet = new HashSet<TBase>();
// Store all existing values
foreach(var obj in standardValues)
{
if(!(obj is TBase value))
{
throw new ArgumentException($"The standard values must be of type {nameof(TBase)} ({baseType}).", nameof(standardValues));
}
valuesSet.Add(value);
valuesList.Add(value);
}
// Get all to-be-mapped values, as dynamic for arithmetics
foreach(dynamic obj in Enum.GetValuesAsUnderlyingType(derivedType))
{
unchecked
{
var derivedValue = (TDerived)obj;
// Get the mapped value
var value = (TBase)(dynamic)((ulong)obj ^ typeHash);
if(valuesSet.Add(value))
{
valuesList.Add(value);
derivedMap[value] = derivedValue;
baseMap[derivedValue] = value;
}else{
// Try the hash code of the reference
var refHashCode = RuntimeHelpers.GetHashCode((object)obj);
// Cast to the underlying type
dynamic initial = Convert.ChangeType((TBase)(dynamic)refHashCode, underlyingType);
// Loop over all values until we get back
dynamic attempt = initial;
do
{
var attemptValue = (TBase)attempt;
if(valuesSet.Add(attemptValue))
{
// This value was not used yet
valuesList.Add(attemptValue);
derivedMap[attemptValue] = derivedValue;
baseMap[derivedValue] = attemptValue;
attempt = null!;
break;
}
}while(++attempt != initial);

if((object?)attempt != null)
{
throw new NotSupportedException($"The values of {nameof(TDerived)} ({derivedType}) do not fit into {nameof(TBase)} ({baseType}).");
}
}
}
}
// All values have been mapped
valuesCollection = new(valuesList);
}
public override Type GetReflectionType(Type objectType, object? instance)
{
if(instance is TBase value && derivedMap.ContainsKey(value))
{
return derivedType;
}
return base.GetReflectionType(objectType, instance);
}
public override ICustomTypeDescriptor? GetTypeDescriptor(Type objectType, object? instance)
{
if(instance is TBase value && derivedMap.TryGetValue(value, out var derived))
{
// Delegate to the descriptor of derived
return new Descriptor(this, TypeDescriptor.GetProvider(derivedType).GetTypeDescriptor(derived));
}
return new Descriptor(this, base.GetTypeDescriptor(objectType, instance));
}
class Descriptor : CustomTypeDescriptor
{
readonly DerivedEnumProvider<TDerived, TBase> provider;
public Descriptor(DerivedEnumProvider<TDerived, TBase> provider, ICustomTypeDescriptor? parent) : base(parent)
{
this.provider = provider;
}
public override TypeConverter? GetConverter()
{
return new Converter(provider, TypeDescriptor.GetConverter(derivedType));
}
}
bool BaseToDerived(TBase baseValue, [NotNullWhen(true)] out object? derivedValue)
{
return derivedMap.TryGetValue(baseValue, out derivedValue);
}
bool DerivedToBase(TDerived derivedValue, [NotNullWhen(true)] out object? baseValue)
{
return baseMap.TryGetValue(derivedValue, out baseValue);
}
class Converter : TypeConverter
{
static readonly Type stringType = typeof(string);
readonly DerivedEnumProvider<TDerived, TBase> provider;
TypeConverter baseConverter => provider.baseConverter;
readonly TypeConverter derivedConverter;
public Converter(DerivedEnumProvider<TDerived, TBase> provider, TypeConverter derivedConverter)
{
this.provider = provider;
this.derivedConverter = derivedConverter;
}
public override bool CanConvertFrom(ITypeDescriptorContext? context, Type sourceType)
{
return derivedType.Equals(sourceType) || baseConverter.CanConvertFrom(context, sourceType);
}
public override bool CanConvertTo(ITypeDescriptorContext? context, Type? destinationType)
{
return derivedType.Equals(destinationType) || baseConverter.CanConvertTo(context, destinationType);
}
public override object? ConvertFrom(ITypeDescriptorContext? context, CultureInfo? culture, object value)
{
if(value is TDerived derivedValue)
{
if(provider.DerivedToBase(derivedValue, out var result))
{
return result;
}
// Do not allow other derived conversions
return null;
}else if(value is string stringValue)
{
if(derivedConverter.ConvertFrom(context, culture, stringValue) is TDerived derivedValue2)
{
if(provider.DerivedToBase(derivedValue2, out var result))
{
// Prefer derived conversion
return result;
}
}
}
// Prefer base conversion
return
baseConverter.ConvertFrom(context, culture, value) ??
TryGetBase(derivedConverter.ConvertFrom(context, culture, value));
}
public override object? ConvertTo(ITypeDescriptorContext? context, CultureInfo? culture, object? value, Type destinationType)
{
if(value is TBase baseValue)
{
if(provider.BaseToDerived(baseValue, out var result))
{
if(derivedType.Equals(destinationType))
{
return result;
}else if(stringType.Equals(destinationType))
{
// Prefer derived conversion
return derivedConverter.ConvertTo(context, culture, result, stringType);
}
// Prefer base conversion
return
baseConverter.ConvertTo(context, culture, value, destinationType) ??
derivedConverter.ConvertTo(context, culture, result, destinationType);
}
}
return baseConverter.ConvertTo(context, culture, value, destinationType);
}
public override StandardValuesCollection? GetStandardValues(ITypeDescriptorContext? context)
{
return provider.valuesCollection;
}
public override bool GetStandardValuesExclusive(ITypeDescriptorContext? context)
{
return true;
}
public override bool GetStandardValuesSupported(ITypeDescriptorContext? context)
{
return true;
}
public override bool IsValid(ITypeDescriptorContext? context, object? value)
{
if(value is TBase baseValue)
{
if(provider.BaseToDerived(baseValue, out var derived))
{
return derivedConverter.IsValid(derived);
}
}
return baseConverter.IsValid(context, value);
}
public override object? CreateInstance(ITypeDescriptorContext? context, IDictionary propertyValues)
{
return
baseConverter.CreateInstance(context, propertyValues) ??
TryGetBase(derivedConverter.CreateInstance(context, propertyValues));
}
private object? TryGetBase(object? derived)
{
return derived is TDerived derivedValue && provider.DerivedToBase(derivedValue, out var baseValue) ? baseValue : null;
}
public override bool GetCreateInstanceSupported(ITypeDescriptorContext? context)
{
return baseConverter.GetCreateInstanceSupported(context) || derivedConverter.GetCreateInstanceSupported(context);
}
public override PropertyDescriptorCollection? GetProperties(ITypeDescriptorContext? context, object value, Attribute[]? attributes)
{
if(value is TBase baseValue && provider.derivedMap.TryGetValue(baseValue, out var derivedValue))
{
return derivedConverter.GetProperties(context, derivedValue, attributes);
}
return baseConverter.GetProperties(context, value, attributes);
}
public override bool GetPropertiesSupported(ITypeDescriptorContext? context)
{
return baseConverter.GetPropertiesSupported(context) || derivedConverter.GetPropertiesSupported(context);
}
}
static readonly object syncRoot = new object();
public static void MakeDerived()
{
lock(syncRoot)
{
// Check that the converter is usable
var converter = TypeDescriptor.GetConverter(baseType);
if(!(
converter.GetStandardValuesSupported() &&
converter.GetStandardValuesExclusive() &&
converter.GetStandardValues() is { Count: > 0 } standardValues
))
{
throw new NotSupportedException($"{nameof(TBase)} ({baseType}) does not provide an exclusive list of values.");
}
var parent = TypeDescriptor.GetProvider(baseType);
// Construct and add the provider
var derived = new DerivedEnumProvider<TDerived, TBase>(parent, converter, standardValues);
TypeDescriptor.AddProvider(derived, baseType);
}
}
}

它可以这样使用:

DerivedEnumProvider<CustomConsoleColor, ConsoleColor>.MakeDerived();
var converter = TypeDescriptor.GetConverter(typeof(ConsoleColor));
foreach(ConsoleColor value in converter.GetStandardValues())
{
Console.WriteLine($"{value} = {converter.ConvertToInvariantString(value)} ({TypeDescriptor.GetReflectionType(value).Name})");
}
enum CustomConsoleColor
{
Ochre,
Olive,
Lime,
Scarlet
}

它打印的东西像:

Black = Black (ConsoleColor)
DarkBlue = DarkBlue (ConsoleColor)
DarkGreen = DarkGreen (ConsoleColor)
DarkCyan = DarkCyan (ConsoleColor)
DarkRed = DarkRed (ConsoleColor)
DarkMagenta = DarkMagenta (ConsoleColor)
DarkYellow = DarkYellow (ConsoleColor)
Gray = Gray (ConsoleColor)
DarkGray = DarkGray (ConsoleColor)
Blue = Blue (ConsoleColor)
Green = Green (ConsoleColor)
Cyan = Cyan (ConsoleColor)
Red = Red (ConsoleColor)
Magenta = Magenta (ConsoleColor)
Yellow = Yellow (ConsoleColor)
White = White (ConsoleColor)
54267293 = Ochre (CustomConsoleColor)
54267292 = Olive (CustomConsoleColor)
54267295 = Lime (CustomConsoleColor)
54267294 = Scarlet (CustomConsoleColor)

这样的壮举是可能的,这要归功于下面的";技巧";,在System.ComponentModel:中使用各种类型

  • CCD_;修改";现有类型,并添加自定义属性、转换等。所有这些都仅限于System.ComponentModel中的其他类型,因此,如果您坚持低级别反射,则不会出现任何问题,但更高级的应用程序(如表单中的组件设计器)会选择这些修改
  • TypeDescriptor.GetReflectionType返回指定用于(低级别)反射的类型。如果您想查找特定枚举字段的属性,这听起来像是要使用的方法。当然,在这一点上,您还可以创建一个自定义的Type实例(例如使用TypeDelegator),它伪造所有新添加的字段,这有点复杂,但可能会更好地与现有的基于反射的代码集成
  • TypeConverter.GetStandardValues是大多数设计时组件实际用来获得可用值集合的东西,它与枚举或其他类似类型(boolean,cultures…)配合得很好。理论上,非枚举类型可以以类似的方式扩展

应该注意的是,这更多的是概念的证明,因此有一些事情需要注意:

  • 枚举应该具有相同的基础类型。这不是一个基本的限制,可以取消
  • 不支持标志(由GetStandardValuesExclusive指示)。这将需要对组合标志时的情况进行更复杂的处理,并且可能不适用于当前的方法
  • TDerived枚举本身不应派生自或以其他方式进行修改。从理论上讲,它根本不必是枚举,可以使用任何任意的键/值对集合,并根据需要创建映射
  • 将一个枚举值映射到另一个枚举的代码可能是次优的。这没什么大不了的,因为每对枚举只需要调用一次,但可以有更好的分布或映射策略。该代码首先尝试将每个单独的值与该类型的哈希代码进行异或,因此这些值可以形成一种"异或";连续的";范围,但如果这不起作用;"随机";尝试值
  • TypeConverter的实施可能并不完整;可以考虑其他转换,有时顺序可能不直观
  • 重复的名称会被考虑,因为它只影响字符串解析。实现附加处理应该相当容易

最新更新