From b5249a3e0ef6b221e3e82ff0d62b6558bf29e15e Mon Sep 17 00:00:00 2001 From: Heath Baron-Morgan Date: Sat, 2 Jul 2022 22:52:47 -0700 Subject: [PATCH 1/6] Add generic Enum.TryFormat method --- .../System.Private.CoreLib/src/System/Enum.cs | 221 ++++++++++-- .../System.Runtime/ref/System.Runtime.cs | 1 + .../System.Runtime/tests/System/EnumTests.cs | 323 ++++++++++++++++++ 3 files changed, 522 insertions(+), 23 deletions(-) diff --git a/src/libraries/System.Private.CoreLib/src/System/Enum.cs b/src/libraries/System.Private.CoreLib/src/System/Enum.cs index d35f3a46c46bf..6dd02d468b61a 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Enum.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Enum.cs @@ -29,6 +29,7 @@ public abstract partial class Enum : ValueType, IComparable, IFormattable, IConv { #region Private Constants private const char EnumSeparatorChar = ','; + private const string ZeroEnumValue = "0"; #endregion #region Private Static Methods @@ -56,6 +57,22 @@ private string ValueToString() }; } + private static bool ValueToString(TEnum value, Span destination, out int charsWritten, bool isHexFormat = false) where TEnum : struct, Enum + { + return (Type.GetTypeCode(typeof(TEnum))) switch + { + TypeCode.SByte => Unsafe.As(ref value).TryFormat(destination, out charsWritten, isHexFormat ? "X2" : null), + TypeCode.Byte => Unsafe.As(ref value).TryFormat(destination, out charsWritten, isHexFormat ? "X2" : null), + TypeCode.Int16 => Unsafe.As(ref value).TryFormat(destination, out charsWritten, isHexFormat ? "X4" : null), + TypeCode.UInt16 => Unsafe.As(ref value).TryFormat(destination, out charsWritten, isHexFormat ? "X4" : null), + TypeCode.UInt32 => Unsafe.As(ref value).TryFormat(destination, out charsWritten, isHexFormat ? "X8" : null), + TypeCode.Int32 => Unsafe.As(ref value).TryFormat(destination, out charsWritten, isHexFormat ? "X8" : null), + TypeCode.UInt64 => Unsafe.As(ref value).TryFormat(destination, out charsWritten, isHexFormat ? "X16" : null), + TypeCode.Int64 => Unsafe.As(ref value).TryFormat(destination, out charsWritten, isHexFormat ? "X16" : null), + _ => throw new InvalidOperationException(SR.InvalidOperation_UnknownEnumType), + }; + } + private string ValueToHexString() { ref byte data = ref this.GetRawData(); @@ -147,10 +164,41 @@ private static string ValueToHexString(object value) { return GetEnumName(enumInfo, value); } - else // These are flags OR'ed together (We treat everything as unsigned types) + + // These are flags OR'ed together (We treat everything as unsigned types) + return InternalFlagsFormat(enumInfo, value); + } + + private static bool InternalFormat(RuntimeType enumType, ulong value, Span destination, out int charsWritten, out bool isDestinationTooSmall) + { + EnumInfo enumInfo = GetEnumInfo(enumType); + + if (!enumInfo.HasFlagsAttribute) { - return InternalFlagsFormat(enumInfo, value); + string? enumName = GetEnumName(enumInfo, value); + + if (enumName == null) + { + charsWritten = 0; + isDestinationTooSmall = false; + return false; + } + + if (destination.Length < enumName.Length) + { + charsWritten = 0; + isDestinationTooSmall = true; + return false; + } + + enumName.CopyTo(destination); + charsWritten = enumName.Length; + isDestinationTooSmall = false; + return true; } + + // These are flags OR'ed together (We treat everything as unsigned types) + return InternalFlagsFormat(enumInfo, value, destination, out charsWritten, out isDestinationTooSmall); } private static string? InternalFlagsFormat(RuntimeType enumType, ulong result) @@ -164,14 +212,54 @@ private static string ValueToHexString(object value) ulong[] values = enumInfo.Values; Debug.Assert(names.Length == values.Length); - // Values are sorted, so if the incoming value is 0, we can check to see whether - // the first entry matches it, in which case we can return its name; otherwise, - // we can just return "0". - if (resultValue == 0) + string? singleEnumFlagsFormat = GetSingleEnumFlagsFormat(ref resultValue, names, values, out int index); + if (singleEnumFlagsFormat != null) { - return values.Length > 0 && values[0] == 0 ? - names[0] : - "0"; + return singleEnumFlagsFormat; + } + + // With a ulong result value, regardless of the enum's base type, the maximum + // possible number of consistent name/values we could have is 64, since every + // value is made up of one or more bits, and when we see values and incorporate + // their names, we effectively switch off those bits. + Span foundItems = stackalloc int[64]; + bool isEnumsFound = FindEnums(ref resultValue, names, values, index, ref foundItems, out int resultLength, out int foundItemsCount); + if (!isEnumsFound) + { + return null; + } + + Debug.Assert(foundItemsCount > 0); + int length = GetMultipleEnumsFlagsFormatResultLength(resultLength, foundItemsCount); + + string multipleEnumsFlagsFormat = string.FastAllocateString(length); + Span resultSpan = new Span(ref multipleEnumsFlagsFormat.GetRawStringData(), multipleEnumsFlagsFormat.Length); + + SetMultipleEnumsFlagsFormat(names, ref foundItems, foundItemsCount, resultSpan); + + return multipleEnumsFlagsFormat; + } + + private static bool InternalFlagsFormat(EnumInfo enumInfo, ulong resultValue, Span destination, out int charsWritten, out bool isDestinationTooSmall) + { + string[] names = enumInfo.Names; + ulong[] values = enumInfo.Values; + Debug.Assert(names.Length == values.Length); + + string? singleEnumFlagsFormat = GetSingleEnumFlagsFormat(ref resultValue, names, values, out int index); + if (singleEnumFlagsFormat != null) + { + if (destination.Length < singleEnumFlagsFormat.Length) + { + charsWritten = 0; + isDestinationTooSmall = true; + return false; + } + + singleEnumFlagsFormat.CopyTo(destination); + charsWritten = singleEnumFlagsFormat.Length; + isDestinationTooSmall = false; + return true; } // With a ulong result value, regardless of the enum's base type, the maximum @@ -179,11 +267,53 @@ private static string ValueToHexString(object value) // value is made up of one or more bits, and when we see values and incorporate // their names, we effectively switch off those bits. Span foundItems = stackalloc int[64]; + bool isEnumsFound = FindEnums(ref resultValue, names, values, index, ref foundItems, out int resultLength, out int foundItemsCount); + if (!isEnumsFound) + { + charsWritten = 0; + isDestinationTooSmall = false; + return false; + } + + Debug.Assert(foundItemsCount > 0); + int length = GetMultipleEnumsFlagsFormatResultLength(resultLength, foundItemsCount); + + if (destination.Length < length) + { + charsWritten = 0; + isDestinationTooSmall = true; + return false; + } + + SetMultipleEnumsFlagsFormat(names, ref foundItems, foundItemsCount, destination); + charsWritten = length; + isDestinationTooSmall = false; + return true; + } + + private static int GetMultipleEnumsFlagsFormatResultLength(int resultLength, int foundItemsCount) + { + const int SeparatorStringLength = 2; // ", " + return checked(resultLength + (SeparatorStringLength * (foundItemsCount - 1))); + } + + private static string? GetSingleEnumFlagsFormat(ref ulong resultValue, string[] names, ulong[] values, out int index) + { + // Values are sorted, so if the incoming value is 0, we can check to see whether + // the first entry matches it, in which case we can return its name; otherwise, + // we can just return "0". + if (resultValue == 0) + { + index = 0; + return values.Length > 0 && values[0] == 0 ? + names[0] : + ZeroEnumValue; + } // Walk from largest to smallest. It's common to have a flags enum with a single // value that matches a single entry, in which case we can just return the existing // name string. - int index = values.Length - 1; + index = values.Length - 1; while (index >= 0) { if (values[index] == resultValue) @@ -199,9 +329,16 @@ private static string ValueToHexString(object value) index--; } + return null; + } + + private static bool FindEnums(ref ulong resultValue, string[] names, ulong[] values, int index, ref Span foundItems, out int resultLength, out int foundItemsCount) + { // Now look for multiple matches, storing the indices of the values // into our span. - int resultLength = 0, foundItemsCount = 0; + resultLength = 0; + foundItemsCount = 0; + while (index >= 0) { ulong currentValue = values[index]; @@ -224,18 +361,13 @@ private static string ValueToHexString(object value) // a non-zero result, we couldn't match the result to only named values. // In that case, we return null and let the call site just generate // a string for the integral value. - if (resultValue != 0) - { - return null; - } + return resultValue == 0; + } + private static void SetMultipleEnumsFlagsFormat(string[] names, ref Span foundItems, int foundItemsCount, Span resultSpan) + { // We know what strings to concatenate. Do so. - Debug.Assert(foundItemsCount > 0); - const int SeparatorStringLength = 2; // ", " - string result = string.FastAllocateString(checked(resultLength + (SeparatorStringLength * (foundItemsCount - 1)))); - - Span resultSpan = new Span(ref result.GetRawStringData(), result.Length); string name = names[foundItems[--foundItemsCount]]; name.CopyTo(resultSpan); resultSpan = resultSpan.Slice(name.Length); @@ -249,9 +381,6 @@ private static string ValueToHexString(object value) name.CopyTo(resultSpan); resultSpan = resultSpan.Slice(name.Length); } - Debug.Assert(resultSpan.IsEmpty); - - return result; } internal static ulong ToUInt64(object value) @@ -1134,6 +1263,52 @@ public static string Format(Type enumType, object value, [StringSyntax(StringSyn throw new FormatException(SR.Format_InvalidEnumFormatSpecification); } + + public static bool TryFormat(TEnum value, Span destination, out int charsWritten, [StringSyntax(StringSyntaxAttribute.EnumFormat)] ReadOnlySpan format) where TEnum : struct, Enum + { + if (!destination.IsEmpty && format.Length == 1) + { + // Optimize for these standard formats that are not affected by culture. + switch ((char)(format[0] | 0x20)) + { + case 'g': + { + RuntimeType rtType = (RuntimeType)typeof(TEnum); + if (InternalFormat(rtType, ToUInt64(value), destination, out charsWritten, out bool isDestinationTooSmall)) + { + return true; + } + if (isDestinationTooSmall) + { + // Avoid calling ValueToString, InternalFormat was possible, however the destination span was too small. + return false; + } + return ValueToString(value, destination, out charsWritten); + } + case 'd': + return ValueToString(value, destination, out charsWritten); + case 'x': + return ValueToString(value, destination, out charsWritten, true); + case 'f': + { + EnumInfo enumInfo = GetEnumInfo((RuntimeType)typeof(TEnum)); + if (InternalFlagsFormat(enumInfo, ToUInt64(value), destination, out charsWritten, out bool isDestinationTooSmall)) + { + return true; + } + if (isDestinationTooSmall) + { + // Avoid calling ValueToString, InternalFlagsFormat was possible, however the destination span was too small. + return false; + } + return ValueToString(value, destination, out charsWritten); + } + } + } + + charsWritten = 0; + return false; + } #endregion #region Private Methods diff --git a/src/libraries/System.Runtime/ref/System.Runtime.cs b/src/libraries/System.Runtime/ref/System.Runtime.cs index c26b436f41097..47ec3b43aaebe 100644 --- a/src/libraries/System.Runtime/ref/System.Runtime.cs +++ b/src/libraries/System.Runtime/ref/System.Runtime.cs @@ -2366,6 +2366,7 @@ protected Enum() { } public string ToString([System.Diagnostics.CodeAnalysis.StringSyntaxAttribute("EnumFormat")] string? format) { throw null; } [System.ObsoleteAttribute("The provider argument is not used. Use ToString(String) instead.")] public string ToString([System.Diagnostics.CodeAnalysis.StringSyntaxAttribute("EnumFormat")] string? format, System.IFormatProvider? provider) { throw null; } + public static bool TryFormat(TEnum value, System.Span destination, out int charsWritten, [System.Diagnostics.CodeAnalysis.StringSyntaxAttribute("EnumFormat")] System.ReadOnlySpan format) where TEnum : struct { throw null; } public static bool TryParse(System.Type enumType, System.ReadOnlySpan value, bool ignoreCase, [System.Diagnostics.CodeAnalysis.NotNullWhenAttribute(true)] out object? result) { throw null; } public static bool TryParse(System.Type enumType, System.ReadOnlySpan value, [System.Diagnostics.CodeAnalysis.NotNullWhenAttribute(true)] out object? result) { throw null; } public static bool TryParse(System.Type enumType, string? value, bool ignoreCase, [System.Diagnostics.CodeAnalysis.NotNullWhenAttribute(true)] out object? result) { throw null; } diff --git a/src/libraries/System.Runtime/tests/System/EnumTests.cs b/src/libraries/System.Runtime/tests/System/EnumTests.cs index a4cbc4a52ac17..4104f4711c917 100644 --- a/src/libraries/System.Runtime/tests/System/EnumTests.cs +++ b/src/libraries/System.Runtime/tests/System/EnumTests.cs @@ -1986,6 +1986,249 @@ public static IEnumerable ToString_Format_TestData() yield return new object[] { AttributeTargets.Class | AttributeTargets.Delegate, "G", "Class, Delegate" }; } + [Theory] + [InlineData(false, false)] + [InlineData(true, false)] + [InlineData(false, true)] + public static void ToString_TryFormat(bool validateDestinationSpanSizeCheck, bool validateExtraSpanSpaceNotFilled) + { + // Format "D": the decimal equivalent of the value is returned. + // Format "X": value in hex form without a leading "0x" + // Format "F": value is treated as a bit field that contains one or more flags that consist of one or more bits. + // If value is equal to a combination of named enumerated constants, a delimiter-separated list of the names + // of those constants is returned. value is searched for flags, going from the flag with the largest value + // to the smallest value. For each flag that corresponds to a bit field in value, the name of the constant + // is concatenated to the delimiter-separated list. The value of that flag is then excluded from further + // consideration, and the search continues for the next flag. + // If value is not equal to a combination of named enumerated constants, the decimal equivalent of value is returned. + // Format "G": if value is equal to a named enumerated constant, the name of that constant is returned. + // Otherwise, if "[Flags]" present, do as Format "F" - else return the decimal value of "value". + + // "D": SByte + TryFormat(SByteEnum.Min, "D", "-128", validateDestinationSpanSizeCheck, validateExtraSpanSpaceNotFilled); + TryFormat(SByteEnum.One, "D", "1", validateDestinationSpanSizeCheck, validateExtraSpanSpaceNotFilled); + TryFormat(SByteEnum.Two, "D", "2", validateDestinationSpanSizeCheck, validateExtraSpanSpaceNotFilled); + TryFormat((SByteEnum)99, "D", "99", validateDestinationSpanSizeCheck, validateExtraSpanSpaceNotFilled); + TryFormat(SByteEnum.Max, "D", "127", validateDestinationSpanSizeCheck, validateExtraSpanSpaceNotFilled); + + // "D": Byte + TryFormat(ByteEnum.Min, "D", "0", validateDestinationSpanSizeCheck, validateExtraSpanSpaceNotFilled); + TryFormat(ByteEnum.One, "D", "1", validateDestinationSpanSizeCheck, validateExtraSpanSpaceNotFilled); + TryFormat(ByteEnum.Two, "D", "2", validateDestinationSpanSizeCheck, validateExtraSpanSpaceNotFilled); + TryFormat((ByteEnum)99, "D", "99", validateDestinationSpanSizeCheck, validateExtraSpanSpaceNotFilled); + TryFormat(ByteEnum.Max, "D", "255", validateDestinationSpanSizeCheck, validateExtraSpanSpaceNotFilled); + + // "D": Int16 + TryFormat(Int16Enum.Min, "D", "-32768", validateDestinationSpanSizeCheck, validateExtraSpanSpaceNotFilled); + TryFormat(Int16Enum.One, "D", "1", validateDestinationSpanSizeCheck, validateExtraSpanSpaceNotFilled); + TryFormat(Int16Enum.Two, "D", "2", validateDestinationSpanSizeCheck, validateExtraSpanSpaceNotFilled); + TryFormat((Int16Enum)99, "D", "99", validateDestinationSpanSizeCheck, validateExtraSpanSpaceNotFilled); + TryFormat(Int16Enum.Max, "D", "32767", validateDestinationSpanSizeCheck, validateExtraSpanSpaceNotFilled); + + // "D": UInt16 + TryFormat(UInt16Enum.Min, "D", "0", validateDestinationSpanSizeCheck, validateExtraSpanSpaceNotFilled); + TryFormat(UInt16Enum.One, "D", "1", validateDestinationSpanSizeCheck, validateExtraSpanSpaceNotFilled); + TryFormat(UInt16Enum.Two, "D", "2", validateDestinationSpanSizeCheck, validateExtraSpanSpaceNotFilled); + TryFormat((UInt16Enum)99, "D", "99", validateDestinationSpanSizeCheck, validateExtraSpanSpaceNotFilled); + TryFormat(UInt16Enum.Max, "D", "65535", validateDestinationSpanSizeCheck, validateExtraSpanSpaceNotFilled); + + // "D": Int32 + TryFormat(Int32Enum.Min, "D", "-2147483648", validateDestinationSpanSizeCheck, validateExtraSpanSpaceNotFilled); + TryFormat(Int32Enum.One, "D", "1", validateDestinationSpanSizeCheck, validateExtraSpanSpaceNotFilled); + TryFormat(Int32Enum.Two, "D", "2", validateDestinationSpanSizeCheck, validateExtraSpanSpaceNotFilled); + TryFormat((Int32Enum)99, "D", "99", validateDestinationSpanSizeCheck, validateExtraSpanSpaceNotFilled); + TryFormat(Int32Enum.Max, "D", "2147483647", validateDestinationSpanSizeCheck, validateExtraSpanSpaceNotFilled); + + // "D": UInt32 + TryFormat(UInt32Enum.Min, "D", "0", validateDestinationSpanSizeCheck, validateExtraSpanSpaceNotFilled); + TryFormat(UInt32Enum.One, "D", "1", validateDestinationSpanSizeCheck, validateExtraSpanSpaceNotFilled); + TryFormat(UInt32Enum.Two, "D", "2", validateDestinationSpanSizeCheck, validateExtraSpanSpaceNotFilled); + TryFormat((UInt32Enum)99, "D", "99", validateDestinationSpanSizeCheck, validateExtraSpanSpaceNotFilled); + TryFormat(UInt32Enum.Max, "D", "4294967295", validateDestinationSpanSizeCheck, validateExtraSpanSpaceNotFilled); + + // "D": Int64 + TryFormat(Int64Enum.Min, "D", "-9223372036854775808", validateDestinationSpanSizeCheck, validateExtraSpanSpaceNotFilled); + TryFormat(Int64Enum.One, "D", "1", validateDestinationSpanSizeCheck, validateExtraSpanSpaceNotFilled); + TryFormat(Int64Enum.Two, "D", "2", validateDestinationSpanSizeCheck, validateExtraSpanSpaceNotFilled); + TryFormat((Int64Enum)99, "D", "99", validateDestinationSpanSizeCheck, validateExtraSpanSpaceNotFilled); + TryFormat(Int64Enum.Max, "D", "9223372036854775807", validateDestinationSpanSizeCheck, validateExtraSpanSpaceNotFilled); + + // "D": UInt64 + TryFormat(UInt64Enum.Min, "D", "0", validateDestinationSpanSizeCheck, validateExtraSpanSpaceNotFilled); + TryFormat(UInt64Enum.One, "D", "1", validateDestinationSpanSizeCheck, validateExtraSpanSpaceNotFilled); + TryFormat(UInt64Enum.Two, "D", "2", validateDestinationSpanSizeCheck, validateExtraSpanSpaceNotFilled); + TryFormat((UInt64Enum)99, "D", "99", validateDestinationSpanSizeCheck, validateExtraSpanSpaceNotFilled); + TryFormat(UInt64Enum.Max, "D", "18446744073709551615", validateDestinationSpanSizeCheck, validateExtraSpanSpaceNotFilled); + + // "D": SimpleEnum + TryFormat(SimpleEnum.Red, "D", "1", validateDestinationSpanSizeCheck, validateExtraSpanSpaceNotFilled); + + // "X": SByte + TryFormat(SByteEnum.Min, "X", "80", validateDestinationSpanSizeCheck, validateExtraSpanSpaceNotFilled); + TryFormat(SByteEnum.One, "X", "01", validateDestinationSpanSizeCheck, validateExtraSpanSpaceNotFilled); + TryFormat(SByteEnum.Two, "X", "02", validateDestinationSpanSizeCheck, validateExtraSpanSpaceNotFilled); + TryFormat((SByteEnum)99, "X", "63", validateDestinationSpanSizeCheck, validateExtraSpanSpaceNotFilled); + TryFormat(SByteEnum.Max, "X", "7F", validateDestinationSpanSizeCheck, validateExtraSpanSpaceNotFilled); + + // "X": Byte + TryFormat(ByteEnum.Min, "X", "00", validateDestinationSpanSizeCheck, validateExtraSpanSpaceNotFilled); + TryFormat(ByteEnum.One, "X", "01", validateDestinationSpanSizeCheck, validateExtraSpanSpaceNotFilled); + TryFormat(ByteEnum.Two, "X", "02", validateDestinationSpanSizeCheck, validateExtraSpanSpaceNotFilled); + TryFormat((ByteEnum)99, "X", "63", validateDestinationSpanSizeCheck, validateExtraSpanSpaceNotFilled); + TryFormat(ByteEnum.Max, "X", "FF", validateDestinationSpanSizeCheck, validateExtraSpanSpaceNotFilled); + + // "X": Int16 + TryFormat(Int16Enum.Min, "X", "8000", validateDestinationSpanSizeCheck, validateExtraSpanSpaceNotFilled); + TryFormat(Int16Enum.One, "X", "0001", validateDestinationSpanSizeCheck, validateExtraSpanSpaceNotFilled); + TryFormat(Int16Enum.Two, "X", "0002", validateDestinationSpanSizeCheck, validateExtraSpanSpaceNotFilled); + TryFormat((Int16Enum)99, "X", "0063", validateDestinationSpanSizeCheck, validateExtraSpanSpaceNotFilled); + TryFormat(Int16Enum.Max, "X", "7FFF", validateDestinationSpanSizeCheck, validateExtraSpanSpaceNotFilled); + + // "X": UInt16 + TryFormat(UInt16Enum.Min, "X", "0000", validateDestinationSpanSizeCheck, validateExtraSpanSpaceNotFilled); + TryFormat(UInt16Enum.One, "X", "0001", validateDestinationSpanSizeCheck, validateExtraSpanSpaceNotFilled); + TryFormat(UInt16Enum.Two, "X", "0002", validateDestinationSpanSizeCheck, validateExtraSpanSpaceNotFilled); + TryFormat((UInt16Enum)99, "X", "0063", validateDestinationSpanSizeCheck, validateExtraSpanSpaceNotFilled); + TryFormat(UInt16Enum.Max, "X", "FFFF", validateDestinationSpanSizeCheck, validateExtraSpanSpaceNotFilled); + + // "X": UInt32 + TryFormat(UInt32Enum.Min, "X", "00000000", validateDestinationSpanSizeCheck, validateExtraSpanSpaceNotFilled); + TryFormat(UInt32Enum.One, "X", "00000001", validateDestinationSpanSizeCheck, validateExtraSpanSpaceNotFilled); + TryFormat(UInt32Enum.Two, "X", "00000002", validateDestinationSpanSizeCheck, validateExtraSpanSpaceNotFilled); + TryFormat((UInt32Enum)99, "X", "00000063", validateDestinationSpanSizeCheck, validateExtraSpanSpaceNotFilled); + TryFormat(UInt32Enum.Max, "X", "FFFFFFFF", validateDestinationSpanSizeCheck, validateExtraSpanSpaceNotFilled); + + // "X": Int32 + TryFormat(Int32Enum.Min, "X", "80000000", validateDestinationSpanSizeCheck, validateExtraSpanSpaceNotFilled); + TryFormat(Int32Enum.One, "X", "00000001", validateDestinationSpanSizeCheck, validateExtraSpanSpaceNotFilled); + TryFormat(Int32Enum.Two, "X", "00000002", validateDestinationSpanSizeCheck, validateExtraSpanSpaceNotFilled); + TryFormat((Int32Enum)99, "X", "00000063", validateDestinationSpanSizeCheck, validateExtraSpanSpaceNotFilled); + TryFormat(Int32Enum.Max, "X", "7FFFFFFF", validateDestinationSpanSizeCheck, validateExtraSpanSpaceNotFilled); + + // "X:" Int64 + TryFormat(Int64Enum.Min, "X", "8000000000000000", validateDestinationSpanSizeCheck, validateExtraSpanSpaceNotFilled); + TryFormat(Int64Enum.One, "X", "0000000000000001", validateDestinationSpanSizeCheck, validateExtraSpanSpaceNotFilled); + TryFormat(Int64Enum.Two, "X", "0000000000000002", validateDestinationSpanSizeCheck, validateExtraSpanSpaceNotFilled); + TryFormat((Int64Enum)99, "X", "0000000000000063", validateDestinationSpanSizeCheck, validateExtraSpanSpaceNotFilled); + TryFormat(Int64Enum.Max, "X", "7FFFFFFFFFFFFFFF", validateDestinationSpanSizeCheck, validateExtraSpanSpaceNotFilled); + + // "X": UInt64 + TryFormat(UInt64Enum.Min, "X", "0000000000000000", validateDestinationSpanSizeCheck, validateExtraSpanSpaceNotFilled); + TryFormat(UInt64Enum.One, "X", "0000000000000001", validateDestinationSpanSizeCheck, validateExtraSpanSpaceNotFilled); + TryFormat(UInt64Enum.Two, "X", "0000000000000002", validateDestinationSpanSizeCheck, validateExtraSpanSpaceNotFilled); + TryFormat((UInt64Enum)99, "X", "0000000000000063", validateDestinationSpanSizeCheck, validateExtraSpanSpaceNotFilled); + TryFormat(UInt64Enum.Max, "X", "FFFFFFFFFFFFFFFF", validateDestinationSpanSizeCheck, validateExtraSpanSpaceNotFilled); + + // "X": SimpleEnum + TryFormat(SimpleEnum.Red, "X", "00000001", validateDestinationSpanSizeCheck, validateExtraSpanSpaceNotFilled); + + // "F": SByte + TryFormat(SByteEnum.Min, "F", "Min", validateDestinationSpanSizeCheck, validateExtraSpanSpaceNotFilled); + TryFormat(SByteEnum.One | SByteEnum.Two, "F", "One, Two", validateDestinationSpanSizeCheck, validateExtraSpanSpaceNotFilled); + TryFormat((SByteEnum)5, "F", "5", validateDestinationSpanSizeCheck, validateExtraSpanSpaceNotFilled); + TryFormat(SByteEnum.Max, "F", "Max", validateDestinationSpanSizeCheck, validateExtraSpanSpaceNotFilled); + + // "F": Byte + TryFormat(ByteEnum.Min, "F", "Min", validateDestinationSpanSizeCheck, validateExtraSpanSpaceNotFilled); + TryFormat(ByteEnum.One | ByteEnum.Two, "F", "One, Two", validateDestinationSpanSizeCheck, validateExtraSpanSpaceNotFilled); + TryFormat((ByteEnum)5, "F", "5", validateDestinationSpanSizeCheck, validateExtraSpanSpaceNotFilled); + TryFormat(ByteEnum.Max, "F", "Max", validateDestinationSpanSizeCheck, validateExtraSpanSpaceNotFilled); + + // "F": Int16 + TryFormat(Int16Enum.Min, "F", "Min", validateDestinationSpanSizeCheck, validateExtraSpanSpaceNotFilled); + TryFormat(Int16Enum.One | Int16Enum.Two, "F", "One, Two", validateDestinationSpanSizeCheck, validateExtraSpanSpaceNotFilled); + TryFormat((Int16Enum)5, "F", "5", validateDestinationSpanSizeCheck, validateExtraSpanSpaceNotFilled); + TryFormat(Int16Enum.Max, "F", "Max", validateDestinationSpanSizeCheck, validateExtraSpanSpaceNotFilled); + + // "F": UInt16 + TryFormat(UInt16Enum.Min, "F", "Min", validateDestinationSpanSizeCheck, validateExtraSpanSpaceNotFilled); + TryFormat(UInt16Enum.One | UInt16Enum.Two, "F", "One, Two", validateDestinationSpanSizeCheck, validateExtraSpanSpaceNotFilled); + TryFormat((UInt16Enum)5, "F", "5", validateDestinationSpanSizeCheck, validateExtraSpanSpaceNotFilled); + TryFormat(UInt16Enum.Max, "F", "Max", validateDestinationSpanSizeCheck, validateExtraSpanSpaceNotFilled); + + // "F": Int32 + TryFormat(Int32Enum.Min, "F", "Min", validateDestinationSpanSizeCheck, validateExtraSpanSpaceNotFilled); + TryFormat(Int32Enum.One | Int32Enum.Two, "F", "One, Two", validateDestinationSpanSizeCheck, validateExtraSpanSpaceNotFilled); + TryFormat((Int32Enum)5, "F", "5", validateDestinationSpanSizeCheck, validateExtraSpanSpaceNotFilled); + TryFormat(Int32Enum.Max, "F", "Max", validateDestinationSpanSizeCheck, validateExtraSpanSpaceNotFilled); + + // "F": UInt32 + TryFormat(UInt32Enum.Min, "F", "Min", validateDestinationSpanSizeCheck, validateExtraSpanSpaceNotFilled); + TryFormat(UInt32Enum.One | UInt32Enum.Two, "F", "One, Two", validateDestinationSpanSizeCheck, validateExtraSpanSpaceNotFilled); + TryFormat((UInt32Enum)5, "F", "5", validateDestinationSpanSizeCheck, validateExtraSpanSpaceNotFilled); + TryFormat(UInt32Enum.Max, "F", "Max", validateDestinationSpanSizeCheck, validateExtraSpanSpaceNotFilled); + + // "F": Int64 + TryFormat(Int64Enum.Min, "F", "Min", validateDestinationSpanSizeCheck, validateExtraSpanSpaceNotFilled); + TryFormat(Int64Enum.One | Int64Enum.Two, "F", "One, Two", validateDestinationSpanSizeCheck, validateExtraSpanSpaceNotFilled); + TryFormat((Int64Enum)5, "F", "5", validateDestinationSpanSizeCheck, validateExtraSpanSpaceNotFilled); + TryFormat(Int64Enum.Max, "F", "Max", validateDestinationSpanSizeCheck, validateExtraSpanSpaceNotFilled); + + // "F": UInt64 + TryFormat(UInt64Enum.Min, "F", "Min", validateDestinationSpanSizeCheck, validateExtraSpanSpaceNotFilled); + TryFormat(UInt64Enum.One | UInt64Enum.Two, "F", "One, Two", validateDestinationSpanSizeCheck, validateExtraSpanSpaceNotFilled); + TryFormat((UInt64Enum)5, "F", "5", validateDestinationSpanSizeCheck, validateExtraSpanSpaceNotFilled); + TryFormat(UInt64Enum.Max, "F", "Max", validateDestinationSpanSizeCheck, validateExtraSpanSpaceNotFilled); + + // "F": SimpleEnum + TryFormat(SimpleEnum.Red, "F", "Red", validateDestinationSpanSizeCheck, validateExtraSpanSpaceNotFilled); + TryFormat(SimpleEnum.Blue, "F", "Blue", validateDestinationSpanSizeCheck, validateExtraSpanSpaceNotFilled); + TryFormat((SimpleEnum)99, "F", "99", validateDestinationSpanSizeCheck, validateExtraSpanSpaceNotFilled); + TryFormat((SimpleEnum)0, "F", "0", validateDestinationSpanSizeCheck, validateExtraSpanSpaceNotFilled); // Not found + + // "F": Flags Attribute + TryFormat(AttributeTargets.Class | AttributeTargets.Delegate, "F", "Class, Delegate", validateDestinationSpanSizeCheck, validateExtraSpanSpaceNotFilled); + + // "G": SByte + TryFormat(SByteEnum.Min, "G", "Min", validateDestinationSpanSizeCheck, validateExtraSpanSpaceNotFilled); + TryFormat((SByteEnum)3, "G", "3", validateDestinationSpanSizeCheck, validateExtraSpanSpaceNotFilled); // No [Flags] attribute + TryFormat(SByteEnum.Max, "G", "Max", validateDestinationSpanSizeCheck, validateExtraSpanSpaceNotFilled); + + // "G": Byte + TryFormat(ByteEnum.Min, "G", "Min", validateDestinationSpanSizeCheck, validateExtraSpanSpaceNotFilled); + TryFormat((ByteEnum)0xff, "G", "Max", validateDestinationSpanSizeCheck, validateExtraSpanSpaceNotFilled); + TryFormat((ByteEnum)3, "G", "3", validateDestinationSpanSizeCheck, validateExtraSpanSpaceNotFilled); // No [Flags] attribute + TryFormat(ByteEnum.Max, "G", "Max", validateDestinationSpanSizeCheck, validateExtraSpanSpaceNotFilled); + + // "G": Int16 + TryFormat(Int16Enum.Min, "G", "Min", validateDestinationSpanSizeCheck, validateExtraSpanSpaceNotFilled); + TryFormat((Int16Enum)3, "G", "3", validateDestinationSpanSizeCheck, validateExtraSpanSpaceNotFilled); // No [Flags] attribute + TryFormat(Int16Enum.Max, "G", "Max", validateDestinationSpanSizeCheck, validateExtraSpanSpaceNotFilled); + + // "G": UInt16 + TryFormat(UInt16Enum.Min, "G", "Min", validateDestinationSpanSizeCheck, validateExtraSpanSpaceNotFilled); + TryFormat((UInt16Enum)3, "G", "3", validateDestinationSpanSizeCheck, validateExtraSpanSpaceNotFilled); // No [Flags] attribute + TryFormat(UInt16Enum.Max, "G", "Max", validateDestinationSpanSizeCheck, validateExtraSpanSpaceNotFilled); + + // "G": Int32 + TryFormat(Int32Enum.Min, "G", "Min", validateDestinationSpanSizeCheck, validateExtraSpanSpaceNotFilled); + TryFormat((Int32Enum)3, "G", "3", validateDestinationSpanSizeCheck, validateExtraSpanSpaceNotFilled); // No [Flags] attribute + TryFormat(Int32Enum.Max, "G", "Max", validateDestinationSpanSizeCheck, validateExtraSpanSpaceNotFilled); + + // "G": UInt32 + TryFormat(UInt32Enum.Min, "G", "Min", validateDestinationSpanSizeCheck, validateExtraSpanSpaceNotFilled); + TryFormat((UInt32Enum)3, "G", "3", validateDestinationSpanSizeCheck, validateExtraSpanSpaceNotFilled); // No [Flags] attribute + TryFormat(UInt32Enum.Max, "G", "Max", validateDestinationSpanSizeCheck, validateExtraSpanSpaceNotFilled); + + // "G": Int64 + TryFormat(Int64Enum.Min, "G", "Min", validateDestinationSpanSizeCheck, validateExtraSpanSpaceNotFilled); + TryFormat((Int64Enum)3, "G", "3", validateDestinationSpanSizeCheck, validateExtraSpanSpaceNotFilled); // No [Flags] attribute + TryFormat(Int64Enum.Max, "G", "Max", validateDestinationSpanSizeCheck, validateExtraSpanSpaceNotFilled); + + // "G": UInt64 + TryFormat(UInt64Enum.Min, "G", "Min", validateDestinationSpanSizeCheck, validateExtraSpanSpaceNotFilled); + TryFormat((UInt64Enum)3, "G", "3", validateDestinationSpanSizeCheck, validateExtraSpanSpaceNotFilled); // No [Flags] attribute + TryFormat(UInt64Enum.Max, "G", "Max", validateDestinationSpanSizeCheck, validateExtraSpanSpaceNotFilled); + + // "G": SimpleEnum + TryFormat((SimpleEnum)99, "G", "99", validateDestinationSpanSizeCheck, validateExtraSpanSpaceNotFilled); + TryFormat((SimpleEnum)99, "G", "99", validateDestinationSpanSizeCheck, validateExtraSpanSpaceNotFilled); + TryFormat((SimpleEnum)0, "G", "0", validateDestinationSpanSizeCheck, validateExtraSpanSpaceNotFilled); // Not found + + // "G": Flags Attribute + TryFormat(AttributeTargets.Class | AttributeTargets.Delegate, "G", "Class, Delegate", validateDestinationSpanSizeCheck, validateExtraSpanSpaceNotFilled); + } + #pragma warning disable 618 // ToString with IFormatProvider is marked as Obsolete. [Theory] [MemberData(nameof(ToString_Format_TestData))] @@ -2055,6 +2298,86 @@ public static void Format(Type enumType, object value, string format, string exp Assert.Equal(expected, Enum.Format(enumType, value, format.ToLowerInvariant())); } + // Select test here, to avoid rewriting input params, as MemberData will does work for generic methods + private static void TryFormat(TEnum value, ReadOnlySpan format, string expected, bool validateDestinationSpanSizeCheck, bool validateExtraSpanSpaceNotFilled) where TEnum : struct, Enum + { + if (validateDestinationSpanSizeCheck) + { + TryFormat_WithDestinationSpanSizeTooSmall_ReturnsFalseWithNoCharsWritten(value, format, expected); + } + if (validateExtraSpanSpaceNotFilled) + { + TryFormat_WithDestinationSpanLargerThanExpected_ReturnsTrueWithExpectedAndExtraSpaceNotFilled(value, format, expected); + } + else + { + TryFormat_WithValidParameters_ReturnsTrueWithExpected(value, format, expected); + } + } + + private static void TryFormat_WithValidParameters_ReturnsTrueWithExpected(TEnum value, ReadOnlySpan format, string expected) where TEnum : struct, Enum + { + Span destination = new char[expected.Length]; + + Assert.True(Enum.TryFormat(value, destination, out int charsWritten, format)); + Assert.Equal(expected, destination.ToString()); + Assert.Equal(expected.Length, charsWritten); + } + + private static void TryFormat_WithDestinationSpanSizeTooSmall_ReturnsFalseWithNoCharsWritten(TEnum value, ReadOnlySpan format, string expected) where TEnum : struct, Enum + { + var oneLessThanExpectedLength = expected.Length - 1; + Span destination = new char[oneLessThanExpectedLength]; + + Assert.False(Enum.TryFormat(value, destination, out int charsWritten, format)); + Assert.Equal(new string('\0', oneLessThanExpectedLength), destination.ToString()); + Assert.Equal(0, charsWritten); + } + + private static void TryFormat_WithDestinationSpanLargerThanExpected_ReturnsTrueWithExpectedAndExtraSpaceNotFilled(TEnum value, ReadOnlySpan format, string expected) where TEnum : struct, Enum + { + var oneMoreThanExpectedLength = expected.Length + 1; + Span destination = new char[oneMoreThanExpectedLength]; + + Assert.True(Enum.TryFormat(value, destination, out int charsWritten, format)); + Assert.Equal(expected + '\0', destination.ToString()); + Assert.Equal(expected.Length, charsWritten); + } + + [Fact] + private static void TryFormat_WithEmptySpan_ReturnsFalseWithNoCharsWritten() + { + Span destination = new char[0]; + + Assert.False(Enum.TryFormat(SimpleEnum.Green, destination, out int charsWritten, "G")); + Assert.Equal("", destination.ToString()); + Assert.Equal(0, charsWritten); + } + + [Theory] + [InlineData(null)] + [InlineData("")] + [InlineData(" ")] + [InlineData(" ")] + [InlineData(" \t")] + [InlineData("a")] + [InlineData("ab")] + [InlineData("abc")] + [InlineData("gg")] + [InlineData("dd")] + [InlineData("xx")] + [InlineData("ff")] + private static void TryFormat_WithInvalidFormat_ReturnsFalseWithNoCharsWritten(string format) + { + var expecedEnum = SimpleEnum.Green; + string expected = nameof(expecedEnum); + Span destination = new char[expected.Length]; + + Assert.False(Enum.TryFormat(expecedEnum, destination, out int charsWritten, format)); + Assert.Equal(new string('\0', expected.Length), destination.ToString()); + Assert.Equal(0, charsWritten); + } + [Fact] public static void Format_Invalid() { From 2eec75a3bbe5ac711621afef3c710e6e5970b088 Mon Sep 17 00:00:00 2001 From: Stephen Toub Date: Sat, 12 Nov 2022 15:04:35 -0500 Subject: [PATCH 2/6] Revise TryFormat implementation and code sharing - Add TryFormat XML docs - Make TryFormat's format parameter optional and allow an empty specifier - Make it an exception to specify invalid format specifier in TryFormat - Streamline TryFormat's default specifier case - Add missing cases where feasible for char/bool/nint/nuint underlying types - Reduce duplicate branches in hex formatting methods where e.g. int/uint cases can be shared - Refactor method implementations to better share return blocks - Add AggressiveInlining to places where call sites are finite / overhead is measurable - Remove checked multiplication from length calculation - Remove some branching from code to find name(s) in flags names - Revise multi-flag name writing to reduce bounds checking - Change all format specifier switches to use ASCII casing trick - Outline exception creation from generic methods to reduce specialized code bloat - Rename helper methods to make their functionality clearer - Tweak formatting for consistency within the file --- .../System.Private.CoreLib/src/System/Enum.cs | 534 ++++++++++-------- .../System.Runtime/ref/System.Runtime.cs | 2 +- 2 files changed, 309 insertions(+), 227 deletions(-) diff --git a/src/libraries/System.Private.CoreLib/src/System/Enum.cs b/src/libraries/System.Private.CoreLib/src/System/Enum.cs index 6dd02d468b61a..fc058799df8f1 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Enum.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Enum.cs @@ -24,20 +24,19 @@ namespace System { [Serializable] - [System.Runtime.CompilerServices.TypeForwardedFrom("mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089")] + [TypeForwardedFrom("mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089")] public abstract partial class Enum : ValueType, IComparable, IFormattable, IConvertible { #region Private Constants private const char EnumSeparatorChar = ','; - private const string ZeroEnumValue = "0"; #endregion #region Private Static Methods - private string ValueToString() + private string AsNumberToString() { ref byte data = ref this.GetRawData(); - return (InternalGetCorElementType()) switch + return InternalGetCorElementType() switch { CorElementType.ELEMENT_TYPE_I1 => Unsafe.As(ref data).ToString(), CorElementType.ELEMENT_TYPE_U1 => data.ToString(), @@ -57,23 +56,60 @@ private string ValueToString() }; } - private static bool ValueToString(TEnum value, Span destination, out int charsWritten, bool isHexFormat = false) where TEnum : struct, Enum + [MethodImpl(MethodImplOptions.AggressiveInlining)] // optimizes into a single TryFormat call for all types expressible in C# + private static bool TryAsNumberToString(TEnum value, Span destination, out int charsWritten) where TEnum : struct, Enum { - return (Type.GetTypeCode(typeof(TEnum))) switch + switch (Type.GetTypeCode(typeof(TEnum))) { - TypeCode.SByte => Unsafe.As(ref value).TryFormat(destination, out charsWritten, isHexFormat ? "X2" : null), - TypeCode.Byte => Unsafe.As(ref value).TryFormat(destination, out charsWritten, isHexFormat ? "X2" : null), - TypeCode.Int16 => Unsafe.As(ref value).TryFormat(destination, out charsWritten, isHexFormat ? "X4" : null), - TypeCode.UInt16 => Unsafe.As(ref value).TryFormat(destination, out charsWritten, isHexFormat ? "X4" : null), - TypeCode.UInt32 => Unsafe.As(ref value).TryFormat(destination, out charsWritten, isHexFormat ? "X8" : null), - TypeCode.Int32 => Unsafe.As(ref value).TryFormat(destination, out charsWritten, isHexFormat ? "X8" : null), - TypeCode.UInt64 => Unsafe.As(ref value).TryFormat(destination, out charsWritten, isHexFormat ? "X16" : null), - TypeCode.Int64 => Unsafe.As(ref value).TryFormat(destination, out charsWritten, isHexFormat ? "X16" : null), - _ => throw new InvalidOperationException(SR.InvalidOperation_UnknownEnumType), + case TypeCode.SByte: + return Unsafe.As(ref value).TryFormat(destination, out charsWritten); + + case TypeCode.Byte: + return Unsafe.As(ref value).TryFormat(destination, out charsWritten); + + case TypeCode.Boolean: + return Unsafe.As(ref value).TryFormat(destination, out charsWritten); + + case TypeCode.Int16: + return Unsafe.As(ref value).TryFormat(destination, out charsWritten); + + case TypeCode.UInt16: + return Unsafe.As(ref value).TryFormat(destination, out charsWritten); + + case TypeCode.Char: + if (!destination.IsEmpty) + { + destination[0] = Unsafe.As(ref value); + charsWritten = 1; + return true; + } + charsWritten = 0; + return false; + + case TypeCode.UInt32: + return Unsafe.As(ref value).TryFormat(destination, out charsWritten); + + case TypeCode.Int32: + return Unsafe.As(ref value).TryFormat(destination, out charsWritten); + + case TypeCode.UInt64: + return Unsafe.As(ref value).TryFormat(destination, out charsWritten); + + case TypeCode.Int64: + return Unsafe.As(ref value).TryFormat(destination, out charsWritten); + + case TypeCode.Single: + return Unsafe.As(ref value).TryFormat(destination, out charsWritten); + + case TypeCode.Double: + return Unsafe.As(ref value).TryFormat(destination, out charsWritten); + + default: + throw new InvalidOperationException(SR.InvalidOperation_UnknownEnumType); }; } - private string ValueToHexString() + private string AsNumberToHexString() { ref byte data = ref this.GetRawData(); Span bytes = stackalloc byte[8]; @@ -85,24 +121,29 @@ private string ValueToHexString() bytes[0] = data; length = 1; break; + case CorElementType.ELEMENT_TYPE_BOOLEAN: return data != 0 ? "01" : "00"; + case CorElementType.ELEMENT_TYPE_I2: case CorElementType.ELEMENT_TYPE_U2: case CorElementType.ELEMENT_TYPE_CHAR: BinaryPrimitives.WriteUInt16BigEndian(bytes, Unsafe.As(ref data)); length = 2; break; + case CorElementType.ELEMENT_TYPE_I4: case CorElementType.ELEMENT_TYPE_U4: BinaryPrimitives.WriteUInt32BigEndian(bytes, Unsafe.As(ref data)); length = 4; break; + case CorElementType.ELEMENT_TYPE_I8: case CorElementType.ELEMENT_TYPE_U8: BinaryPrimitives.WriteUInt64BigEndian(bytes, Unsafe.As(ref data)); length = 8; break; + default: throw new InvalidOperationException(SR.InvalidOperation_UnknownEnumType); } @@ -110,9 +151,37 @@ private string ValueToHexString() return HexConverter.ToString(bytes.Slice(0, length), HexConverter.Casing.Upper); } - private static string ValueToHexString(object value) + [MethodImpl(MethodImplOptions.AggressiveInlining)] // optimizes into a single TryFormat call for all types expressible in C# + private static bool TryAsNumberToHexString(TEnum value, Span destination, out int charsWritten) where TEnum : struct, Enum { - return (Convert.GetTypeCode(value)) switch + switch (Type.GetTypeCode(typeof(TEnum))) + { + case TypeCode.Byte or TypeCode.SByte: + return Unsafe.As(ref value).TryFormat(destination, out charsWritten, "X2"); + + case TypeCode.Boolean: + { + bool copied = (Unsafe.As(ref value) ? "01" : "00").TryCopyTo(destination); + charsWritten = copied ? 2 : 0; + return copied; + } + + case TypeCode.UInt16 or TypeCode.Int16 or TypeCode.Char: + return Unsafe.As(ref value).TryFormat(destination, out charsWritten, "X4"); + + case TypeCode.UInt32 or TypeCode.Int32: + return Unsafe.As(ref value).TryFormat(destination, out charsWritten, "X8"); + + case TypeCode.UInt64 or TypeCode.Int64: + return Unsafe.As(ref value).TryFormat(destination, out charsWritten, "X16"); + + default: + throw new InvalidOperationException(SR.InvalidOperation_UnknownEnumType); + }; + } + + private static string AsNumberToHexString(object value) => + Convert.GetTypeCode(value) switch { TypeCode.SByte => ((byte)(sbyte)value).ToString("X2", null), TypeCode.Byte => ((byte)value).ToString("X2", null), @@ -126,7 +195,6 @@ private static string ValueToHexString(object value) TypeCode.Int64 => ((ulong)(long)value).ToString("X16", null), _ => throw new InvalidOperationException(SR.InvalidOperation_UnknownEnumType), }; - } internal static string? GetEnumName(RuntimeType enumType, ulong ulValue) { @@ -156,7 +224,7 @@ private static string ValueToHexString(object value) return null; // return null so the caller knows to .ToString() the input } - private static string? InternalFormat(RuntimeType enumType, ulong value) + private static string? FormatSingleNameOrFlagNames(RuntimeType enumType, ulong value) { EnumInfo enumInfo = GetEnumInfo(enumType); @@ -166,138 +234,94 @@ private static string ValueToHexString(object value) } // These are flags OR'ed together (We treat everything as unsigned types) - return InternalFlagsFormat(enumInfo, value); + return FormatFlagNames(enumInfo, value); } - private static bool InternalFormat(RuntimeType enumType, ulong value, Span destination, out int charsWritten, out bool isDestinationTooSmall) - { - EnumInfo enumInfo = GetEnumInfo(enumType); - - if (!enumInfo.HasFlagsAttribute) - { - string? enumName = GetEnumName(enumInfo, value); - - if (enumName == null) - { - charsWritten = 0; - isDestinationTooSmall = false; - return false; - } - - if (destination.Length < enumName.Length) - { - charsWritten = 0; - isDestinationTooSmall = true; - return false; - } - - enumName.CopyTo(destination); - charsWritten = enumName.Length; - isDestinationTooSmall = false; - return true; - } - - // These are flags OR'ed together (We treat everything as unsigned types) - return InternalFlagsFormat(enumInfo, value, destination, out charsWritten, out isDestinationTooSmall); - } - - private static string? InternalFlagsFormat(RuntimeType enumType, ulong result) - { - return InternalFlagsFormat(GetEnumInfo(enumType), result); - } - - private static string? InternalFlagsFormat(EnumInfo enumInfo, ulong resultValue) + private static string? FormatFlagNames(EnumInfo enumInfo, ulong resultValue) { string[] names = enumInfo.Names; ulong[] values = enumInfo.Values; Debug.Assert(names.Length == values.Length); - string? singleEnumFlagsFormat = GetSingleEnumFlagsFormat(ref resultValue, names, values, out int index); - if (singleEnumFlagsFormat != null) + string? result = GetSingleFlagsEnumNameForValue(resultValue, names, values, out int index); + if (result is null) { - return singleEnumFlagsFormat; - } + // With a ulong result value, regardless of the enum's base type, the maximum + // possible number of consistent name/values we could have is 64, since every + // value is made up of one or more bits, and when we see values and incorporate + // their names, we effectively switch off those bits. + Span foundItems = stackalloc int[64]; + if (TryFindFlagsNames(resultValue, names, values, index, foundItems, out int resultLength, out int foundItemsCount)) + { + foundItems = foundItems.Slice(0, foundItemsCount); + int length = GetMultipleEnumsFlagsFormatResultLength(resultLength, foundItemsCount); - // With a ulong result value, regardless of the enum's base type, the maximum - // possible number of consistent name/values we could have is 64, since every - // value is made up of one or more bits, and when we see values and incorporate - // their names, we effectively switch off those bits. - Span foundItems = stackalloc int[64]; - bool isEnumsFound = FindEnums(ref resultValue, names, values, index, ref foundItems, out int resultLength, out int foundItemsCount); - if (!isEnumsFound) - { - return null; + result = string.FastAllocateString(length); + WriteMultipleFoundFlagsNames(names, foundItems, new Span(ref result.GetRawStringData(), result.Length)); + } } - Debug.Assert(foundItemsCount > 0); - int length = GetMultipleEnumsFlagsFormatResultLength(resultLength, foundItemsCount); - - string multipleEnumsFlagsFormat = string.FastAllocateString(length); - Span resultSpan = new Span(ref multipleEnumsFlagsFormat.GetRawStringData(), multipleEnumsFlagsFormat.Length); - - SetMultipleEnumsFlagsFormat(names, ref foundItems, foundItemsCount, resultSpan); - - return multipleEnumsFlagsFormat; + return result; } - private static bool InternalFlagsFormat(EnumInfo enumInfo, ulong resultValue, Span destination, out int charsWritten, out bool isDestinationTooSmall) + private static bool TryFormatFlagNames(EnumInfo enumInfo, ulong resultValue, Span destination, out int charsWritten, ref bool isDestinationTooSmall) { + Debug.Assert(!isDestinationTooSmall); + string[] names = enumInfo.Names; ulong[] values = enumInfo.Values; Debug.Assert(names.Length == values.Length); - string? singleEnumFlagsFormat = GetSingleEnumFlagsFormat(ref resultValue, names, values, out int index); - if (singleEnumFlagsFormat != null) + if (GetSingleFlagsEnumNameForValue(resultValue, names, values, out int index) is string singleEnumFlagsFormat) { - if (destination.Length < singleEnumFlagsFormat.Length) + if (singleEnumFlagsFormat.TryCopyTo(destination)) { - charsWritten = 0; - isDestinationTooSmall = true; - return false; + charsWritten = singleEnumFlagsFormat.Length; + return true; } - singleEnumFlagsFormat.CopyTo(destination); - charsWritten = singleEnumFlagsFormat.Length; - isDestinationTooSmall = false; - return true; + isDestinationTooSmall = true; } - - // With a ulong result value, regardless of the enum's base type, the maximum - // possible number of consistent name/values we could have is 64, since every - // value is made up of one or more bits, and when we see values and incorporate - // their names, we effectively switch off those bits. - Span foundItems = stackalloc int[64]; - bool isEnumsFound = FindEnums(ref resultValue, names, values, index, ref foundItems, out int resultLength, out int foundItemsCount); - if (!isEnumsFound) + else { - charsWritten = 0; - isDestinationTooSmall = false; - return false; - } + // With a ulong result value, regardless of the enum's base type, the maximum + // possible number of consistent name/values we could have is 64, since every + // value is made up of one or more bits, and when we see values and incorporate + // their names, we effectively switch off those bits. + Span foundItems = stackalloc int[64]; + if (TryFindFlagsNames(resultValue, names, values, index, foundItems, out int resultLength, out int foundItemsCount)) + { + foundItems = foundItems.Slice(0, foundItemsCount); + int length = GetMultipleEnumsFlagsFormatResultLength(resultLength, foundItemsCount); - Debug.Assert(foundItemsCount > 0); - int length = GetMultipleEnumsFlagsFormatResultLength(resultLength, foundItemsCount); + if (length <= destination.Length) + { + charsWritten = length; + WriteMultipleFoundFlagsNames(names, foundItems, destination); + return true; + } - if (destination.Length < length) - { - charsWritten = 0; - isDestinationTooSmall = true; - return false; + isDestinationTooSmall = true; + } } - SetMultipleEnumsFlagsFormat(names, ref foundItems, foundItemsCount, destination); - charsWritten = length; - isDestinationTooSmall = false; - return true; + charsWritten = 0; + return false; } + [MethodImpl(MethodImplOptions.AggressiveInlining)] // used twice, once from string-based and once from span-based code path private static int GetMultipleEnumsFlagsFormatResultLength(int resultLength, int foundItemsCount) { + Debug.Assert(foundItemsCount >= 2); + Debug.Assert(foundItemsCount <= 64); + const int SeparatorStringLength = 2; // ", " - return checked(resultLength + (SeparatorStringLength * (foundItemsCount - 1))); + int allSeparatorsLength = SeparatorStringLength * (foundItemsCount - 1); + return checked(resultLength + allSeparatorsLength); } - private static string? GetSingleEnumFlagsFormat(ref ulong resultValue, string[] names, ulong[] values, out int index) + [MethodImpl(MethodImplOptions.AggressiveInlining)] // used twice, once from string-based and once from span-based code path + private static string? GetSingleFlagsEnumNameForValue(ulong resultValue, string[] names, ulong[] values, out int index) { // Values are sorted, so if the incoming value is 0, we can check to see whether // the first entry matches it, in which case we can return its name; otherwise, @@ -307,42 +331,48 @@ private static int GetMultipleEnumsFlagsFormatResultLength(int resultLength, int index = 0; return values.Length > 0 && values[0] == 0 ? names[0] : - ZeroEnumValue; + "0"; } // Walk from largest to smallest. It's common to have a flags enum with a single // value that matches a single entry, in which case we can just return the existing // name string. - index = values.Length - 1; - while (index >= 0) + int i; + for (i = values.Length - 1; (uint)i < (uint)values.Length; i--) { - if (values[index] == resultValue) + if (values[i] <= resultValue) { - return names[index]; - } + if (values[i] == resultValue) + { + index = i; + return names[i]; + } - if (values[index] < resultValue) - { break; } - - index--; } + index = i; return null; } - private static bool FindEnums(ref ulong resultValue, string[] names, ulong[] values, int index, ref Span foundItems, out int resultLength, out int foundItemsCount) + [MethodImpl(MethodImplOptions.AggressiveInlining)] // used twice, once from string-based and once from span-based code path + private static bool TryFindFlagsNames(ulong resultValue, string[] names, ulong[] values, int index, Span foundItems, out int resultLength, out int foundItemsCount) { // Now look for multiple matches, storing the indices of the values // into our span. resultLength = 0; foundItemsCount = 0; - while (index >= 0) + while (true) { + if ((uint)index >= (uint)values.Length) + { + break; + } + ulong currentValue = values[index]; - if (index == 0 && currentValue == 0) + if (((uint)index | currentValue) == 0) { break; } @@ -350,7 +380,8 @@ private static bool FindEnums(ref ulong resultValue, string[] names, ulong[] val if ((resultValue & currentValue) == currentValue) { resultValue -= currentValue; - foundItems[foundItemsCount++] = index; + foundItems[foundItemsCount] = index; + foundItemsCount++; resultLength = checked(resultLength + names[index].Length); } @@ -360,35 +391,33 @@ private static bool FindEnums(ref ulong resultValue, string[] names, ulong[] val // If we exhausted looking through all the values and we still have // a non-zero result, we couldn't match the result to only named values. // In that case, we return null and let the call site just generate - // a string for the integral value. + // a string for the integral value if it desires. return resultValue == 0; } - private static void SetMultipleEnumsFlagsFormat(string[] names, ref Span foundItems, int foundItemsCount, Span resultSpan) + [MethodImpl(MethodImplOptions.AggressiveInlining)] // used twice, once from string-based and once from span-based code path + private static void WriteMultipleFoundFlagsNames(string[] names, ReadOnlySpan foundItems, Span destination) { - // We know what strings to concatenate. Do so. + Debug.Assert(foundItems.Length >= 2); - string name = names[foundItems[--foundItemsCount]]; - name.CopyTo(resultSpan); - resultSpan = resultSpan.Slice(name.Length); - while (--foundItemsCount >= 0) + for (int i = foundItems.Length - 1; i != 0; i--) { - resultSpan[0] = EnumSeparatorChar; - resultSpan[1] = ' '; - resultSpan = resultSpan.Slice(2); - - name = names[foundItems[foundItemsCount]]; - name.CopyTo(resultSpan); - resultSpan = resultSpan.Slice(name.Length); + string name = names[foundItems[i]]; + name.CopyTo(destination); + destination = destination.Slice(name.Length); + Span afterSeparator = destination.Slice(2); // done before copying ", " to eliminate those two bounds checks + destination[0] = EnumSeparatorChar; + destination[1] = ' '; + destination = afterSeparator; } + + names[foundItems[0]].CopyTo(destination); } - internal static ulong ToUInt64(object value) - { + internal static ulong ToUInt64(object value) => // Helper function to silently convert the value to UInt64 from the other base types for enum without throwing an exception. // This is need since the Convert functions do overflow checks. - TypeCode typeCode = Convert.GetTypeCode(value); - ulong result = typeCode switch + Convert.GetTypeCode(value) switch { TypeCode.SByte => (ulong)(sbyte)value, TypeCode.Byte => (byte)value, @@ -402,8 +431,6 @@ internal static ulong ToUInt64(object value) TypeCode.Int64 => (ulong)(long)value, _ => throw new InvalidOperationException(SR.InvalidOperation_UnknownEnumType), }; - return result; - } private static ulong ToUInt64(TEnum value) where TEnum : struct, Enum => Type.GetTypeCode(typeof(TEnum)) switch @@ -514,6 +541,7 @@ public bool HasFlag(Enum flag) byte flagsValue = pFlagsValue; return (pThisValue & flagsValue) == flagsValue; } + case CorElementType.ELEMENT_TYPE_I2: case CorElementType.ELEMENT_TYPE_U2: case CorElementType.ELEMENT_TYPE_CHAR: @@ -521,6 +549,7 @@ public bool HasFlag(Enum flag) ushort flagsValue = Unsafe.As(ref pFlagsValue); return (Unsafe.As(ref pThisValue) & flagsValue) == flagsValue; } + case CorElementType.ELEMENT_TYPE_I4: case CorElementType.ELEMENT_TYPE_U4: #if TARGET_32BIT @@ -532,6 +561,7 @@ public bool HasFlag(Enum flag) uint flagsValue = Unsafe.As(ref pFlagsValue); return (Unsafe.As(ref pThisValue) & flagsValue) == flagsValue; } + case CorElementType.ELEMENT_TYPE_I8: case CorElementType.ELEMENT_TYPE_U8: #if TARGET_64BIT @@ -543,6 +573,7 @@ public bool HasFlag(Enum flag) ulong flagsValue = Unsafe.As(ref pFlagsValue); return (Unsafe.As(ref pThisValue) & flagsValue) == flagsValue; } + default: Debug.Fail("Unknown enum underlying type"); return false; @@ -710,6 +741,7 @@ private static bool TryParse(Type enumType, string? value, bool ignoreCase, bool { ThrowHelper.ThrowArgumentNullException(ExceptionArgument.value); } + result = null; return false; } @@ -727,8 +759,9 @@ private static bool TryParse(Type enumType, ReadOnlySpan value, bool ignor { if (throwOnFailure) { - throw new ArgumentException(SR.Arg_MustContainEnumInfo, nameof(value)); + ThrowInvalidEmptyParseArgument(); } + result = null; return false; } @@ -821,6 +854,7 @@ private static bool TryParse(string? value, bool ignoreCase, bool throwOn { ArgumentNullException.Throw(nameof(value)); } + result = default; return false; } @@ -832,18 +866,19 @@ private static bool TryParse(ReadOnlySpan value, bool ignoreCase, b { // Validation on the enum type itself. Failures here are considered non-parsing failures // and thus always throw rather than returning false. - if (!typeof(TEnum).IsEnum) + if (!typeof(TEnum).IsEnum) // with IsEnum being an intrinsic, this whole block will be eliminated for all meaningful cases { throw new ArgumentException(SR.Arg_MustBeEnum, nameof(TEnum)); } value = value.TrimStart(); - if (value.Length == 0) + if (value.IsEmpty) { if (throwOnFailure) { - throw new ArgumentException(SR.Arg_MustContainEnumInfo, nameof(value)); + ThrowInvalidEmptyParseArgument(); } + result = default; return false; } @@ -1192,9 +1227,7 @@ public static object ToObject(Type enumType, object value) ArgumentNullException.ThrowIfNull(value); // Delegate rest of error checking to the other functions - TypeCode typeCode = Convert.GetTypeCode(value); - - return typeCode switch + return Convert.GetTypeCode(value) switch { TypeCode.Int32 => ToObject(enumType, (int)value), TypeCode.SByte => ToObject(enumType, (sbyte)value), @@ -1227,7 +1260,7 @@ public static string Format(Type enumType, object value, [StringSyntax(StringSyn if (format.Length != 1) { // all acceptable format string are of length 1 - throw new FormatException(SR.Format_InvalidEnumFormatSpecification); + throw CreateInvalidFormatSpecifierException(); } return ((Enum)value).ToString(format); } @@ -1241,73 +1274,99 @@ public static string Format(Type enumType, object value, [StringSyntax(StringSyn if (format.Length == 1) { - switch (format[0]) + switch (format[0] | 0x20) { - case 'G': case 'g': - return InternalFormat(rtType, ToUInt64(value)) ?? value.ToString()!; + return FormatSingleNameOrFlagNames(rtType, ToUInt64(value)) ?? value.ToString()!; - case 'D': case 'd': return value.ToString()!; - case 'X': case 'x': - return ValueToHexString(value); + return AsNumberToHexString(value); - case 'F': case 'f': - return InternalFlagsFormat(rtType, ToUInt64(value)) ?? value.ToString()!; + return FormatFlagNames(GetEnumInfo(rtType), ToUInt64(value)) ?? value.ToString()!; } } - throw new FormatException(SR.Format_InvalidEnumFormatSpecification); + throw CreateInvalidFormatSpecifierException(); } - public static bool TryFormat(TEnum value, Span destination, out int charsWritten, [StringSyntax(StringSyntaxAttribute.EnumFormat)] ReadOnlySpan format) where TEnum : struct, Enum - { - if (!destination.IsEmpty && format.Length == 1) + /// Tries to format the value of the enumerated type instance into the provided span of characters. + /// + /// + /// The span into which to write the instance's value formatted as a span of characters. + /// When this method returns, contains the number of characters that were written in . + /// A span containing the character that represents the standard format string that defines the acceptable format of destination. This may be empty, or "g", "d", "f", or "x". + /// if the formatting was successful; otherwise, if the destination span wasn't large enough to contain the formatted value. + /// The format parameter contains an invalid value. + [MethodImpl(MethodImplOptions.AggressiveInlining)] // format is most frequently a constant, and we want it exposed to the implementation; this should be inlined automatically, anyway + public static bool TryFormat(TEnum value, Span destination, out int charsWritten, [StringSyntax(StringSyntaxAttribute.EnumFormat)] ReadOnlySpan format = default) where TEnum : struct, Enum + { + return format.IsEmpty ? + TryFormatDefault(value, destination, out charsWritten) : + TryFormatNonDefault(value, destination, out charsWritten, format); + + static bool TryFormatDefault(TEnum value, Span destination, out int charsWritten) { - // Optimize for these standard formats that are not affected by culture. - switch ((char)(format[0] | 0x20)) + EnumInfo enumInfo = GetEnumInfo((RuntimeType)typeof(TEnum)); + ulong ulongValue = ToUInt64(value); + + if (!enumInfo.HasFlagsAttribute) { - case 'g': + if (GetEnumName(enumInfo, ulongValue) is string enumName) + { + if (enumName.TryCopyTo(destination)) { - RuntimeType rtType = (RuntimeType)typeof(TEnum); - if (InternalFormat(rtType, ToUInt64(value), destination, out charsWritten, out bool isDestinationTooSmall)) - { - return true; - } - if (isDestinationTooSmall) - { - // Avoid calling ValueToString, InternalFormat was possible, however the destination span was too small. - return false; - } - return ValueToString(value, destination, out charsWritten); + charsWritten = enumName.Length; + return true; } - case 'd': - return ValueToString(value, destination, out charsWritten); - case 'x': - return ValueToString(value, destination, out charsWritten, true); - case 'f': - { - EnumInfo enumInfo = GetEnumInfo((RuntimeType)typeof(TEnum)); - if (InternalFlagsFormat(enumInfo, ToUInt64(value), destination, out charsWritten, out bool isDestinationTooSmall)) - { - return true; - } - if (isDestinationTooSmall) + + charsWritten = 0; + return false; + } + } + else + { + bool destinationIsTooSmall = false; + if (TryFormatFlagNames(enumInfo, ulongValue, destination, out charsWritten, ref destinationIsTooSmall) || destinationIsTooSmall) + { + return !destinationIsTooSmall; + } + } + + return TryAsNumberToString(value, destination, out charsWritten); + } + + static bool TryFormatNonDefault(TEnum value, Span destination, out int charsWritten, ReadOnlySpan format) + { + if (format.Length == 1) + { + switch (format[0] | 0x20) + { + case 'g': + return TryFormatDefault(value, destination, out charsWritten); + + case 'd': + return TryAsNumberToString(value, destination, out charsWritten); + + case 'x': + return TryAsNumberToHexString(value, destination, out charsWritten); + + case 'f': + bool destinationIsTooSmall = false; + if (TryFormatFlagNames(GetEnumInfo((RuntimeType)typeof(TEnum)), ToUInt64(value), destination, out charsWritten, ref destinationIsTooSmall) || + destinationIsTooSmall) { - // Avoid calling ValueToString, InternalFlagsFormat was possible, however the destination span was too small. - return false; + return !destinationIsTooSmall; } - return ValueToString(value, destination, out charsWritten); - } + goto case 'd'; + } } - } - charsWritten = 0; - return false; + throw CreateInvalidFormatSpecifierException(); + } } #endregion @@ -1315,7 +1374,7 @@ public static bool TryFormat(TEnum value, Span destination, out int internal object GetValue() { ref byte data = ref this.GetRawData(); - return (InternalGetCorElementType()) switch + return InternalGetCorElementType() switch { CorElementType.ELEMENT_TYPE_I1 => Unsafe.As(ref data), CorElementType.ELEMENT_TYPE_U1 => data, @@ -1335,6 +1394,7 @@ internal object GetValue() }; } + [MethodImpl(MethodImplOptions.AggressiveInlining)] private ulong ToUInt64() { ref byte data = ref this.GetRawData(); @@ -1342,37 +1402,46 @@ private ulong ToUInt64() { case CorElementType.ELEMENT_TYPE_I1: return (ulong)Unsafe.As(ref data); + case CorElementType.ELEMENT_TYPE_U1: return data; + case CorElementType.ELEMENT_TYPE_BOOLEAN: return data != 0 ? 1UL : 0UL; + case CorElementType.ELEMENT_TYPE_I2: return (ulong)Unsafe.As(ref data); + case CorElementType.ELEMENT_TYPE_U2: case CorElementType.ELEMENT_TYPE_CHAR: return Unsafe.As(ref data); + case CorElementType.ELEMENT_TYPE_I4: #if TARGET_32BIT case CorElementType.ELEMENT_TYPE_I: #endif return (ulong)Unsafe.As(ref data); + case CorElementType.ELEMENT_TYPE_U4: #if TARGET_32BIT case CorElementType.ELEMENT_TYPE_U: #endif case CorElementType.ELEMENT_TYPE_R4: return Unsafe.As(ref data); + case CorElementType.ELEMENT_TYPE_I8: #if TARGET_64BIT case CorElementType.ELEMENT_TYPE_I: #endif return (ulong)Unsafe.As(ref data); + case CorElementType.ELEMENT_TYPE_U8: #if TARGET_64BIT case CorElementType.ELEMENT_TYPE_U: #endif case CorElementType.ELEMENT_TYPE_R8: return Unsafe.As(ref data); + default: Debug.Fail("Unknown enum underlying type"); return 0; @@ -1403,10 +1472,12 @@ public override bool Equals([NotNullWhen(true)] object? obj) case CorElementType.ELEMENT_TYPE_U1: case CorElementType.ELEMENT_TYPE_BOOLEAN: return pThisValue == pOtherValue; + case CorElementType.ELEMENT_TYPE_I2: case CorElementType.ELEMENT_TYPE_U2: case CorElementType.ELEMENT_TYPE_CHAR: return Unsafe.As(ref pThisValue) == Unsafe.As(ref pOtherValue); + case CorElementType.ELEMENT_TYPE_I4: case CorElementType.ELEMENT_TYPE_U4: #if TARGET_32BIT @@ -1415,6 +1486,7 @@ public override bool Equals([NotNullWhen(true)] object? obj) #endif case CorElementType.ELEMENT_TYPE_R4: return Unsafe.As(ref pThisValue) == Unsafe.As(ref pOtherValue); + case CorElementType.ELEMENT_TYPE_I8: case CorElementType.ELEMENT_TYPE_U8: #if TARGET_64BIT @@ -1423,6 +1495,7 @@ public override bool Equals([NotNullWhen(true)] object? obj) #endif case CorElementType.ELEMENT_TYPE_R8: return Unsafe.As(ref pThisValue) == Unsafe.As(ref pOtherValue); + default: Debug.Fail("Unknown enum underlying type"); return false; @@ -1435,7 +1508,7 @@ public override int GetHashCode() // The runtime can bypass calls to Enum::GetHashCode and call the underlying type's GetHashCode directly // to avoid boxing the enum. ref byte data = ref this.GetRawData(); - return (InternalGetCorElementType()) switch + return InternalGetCorElementType() switch { CorElementType.ELEMENT_TYPE_I1 => Unsafe.As(ref data).GetHashCode(), CorElementType.ELEMENT_TYPE_U1 => data.GetHashCode(), @@ -1457,14 +1530,10 @@ public override int GetHashCode() public override string ToString() { - // Returns the value in a human readable format. For PASCAL style enums who's value maps directly the name of the field is returned. - // For PASCAL style enums who's values do not map directly the decimal value of the field is returned. - // For BitFlags (indicated by the Flags custom attribute): If for each bit that is set in the value there is a corresponding constant - // (a pure power of 2), then the OR string (ie "Red, Yellow") is returned. Otherwise, if the value is zero or if you can't create a string that consists of - // pure powers of 2 OR-ed together, you return a hex value - // Try to see if its one of the enum values, then we return a String back else the value - return InternalFormat((RuntimeType)GetType(), ToUInt64()) ?? ValueToString(); + return + FormatSingleNameOrFlagNames((RuntimeType)GetType(), ToUInt64()) ?? + AsNumberToString(); } public int CompareTo(object? target) @@ -1485,38 +1554,48 @@ public int CompareTo(object? target) { case CorElementType.ELEMENT_TYPE_I1: return Unsafe.As(ref pThisValue).CompareTo(Unsafe.As(ref pTargetValue)); + case CorElementType.ELEMENT_TYPE_U1: case CorElementType.ELEMENT_TYPE_BOOLEAN: return pThisValue.CompareTo(pTargetValue); + case CorElementType.ELEMENT_TYPE_I2: return Unsafe.As(ref pThisValue).CompareTo(Unsafe.As(ref pTargetValue)); + case CorElementType.ELEMENT_TYPE_U2: case CorElementType.ELEMENT_TYPE_CHAR: return Unsafe.As(ref pThisValue).CompareTo(Unsafe.As(ref pTargetValue)); + case CorElementType.ELEMENT_TYPE_I4: #if TARGET_32BIT case CorElementType.ELEMENT_TYPE_I: #endif return Unsafe.As(ref pThisValue).CompareTo(Unsafe.As(ref pTargetValue)); + case CorElementType.ELEMENT_TYPE_U4: #if TARGET_32BIT case CorElementType.ELEMENT_TYPE_U: #endif return Unsafe.As(ref pThisValue).CompareTo(Unsafe.As(ref pTargetValue)); + case CorElementType.ELEMENT_TYPE_I8: #if TARGET_64BIT case CorElementType.ELEMENT_TYPE_I: #endif return Unsafe.As(ref pThisValue).CompareTo(Unsafe.As(ref pTargetValue)); + case CorElementType.ELEMENT_TYPE_U8: #if TARGET_64BIT case CorElementType.ELEMENT_TYPE_U: #endif return Unsafe.As(ref pThisValue).CompareTo(Unsafe.As(ref pTargetValue)); + case CorElementType.ELEMENT_TYPE_R4: return Unsafe.As(ref pThisValue).CompareTo(Unsafe.As(ref pTargetValue)); + case CorElementType.ELEMENT_TYPE_R8: return Unsafe.As(ref pThisValue).CompareTo(Unsafe.As(ref pTargetValue)); + default: Debug.Fail("Unknown enum underlying type"); return 0; @@ -1542,27 +1621,23 @@ public string ToString([StringSyntax(StringSyntaxAttribute.EnumFormat)] string? if (format.Length == 1) { - switch (format[0]) + switch (format[0] | 0x20) { - case 'G': case 'g': return ToString(); - case 'D': case 'd': - return ValueToString(); + return AsNumberToString(); - case 'X': case 'x': - return ValueToHexString(); + return AsNumberToHexString(); - case 'F': case 'f': - return InternalFlagsFormat((RuntimeType)GetType(), ToUInt64()) ?? ValueToString(); + return FormatFlagNames(GetEnumInfo((RuntimeType)GetType()), ToUInt64()) ?? AsNumberToString(); } } - throw new FormatException(SR.Format_InvalidEnumFormatSpecification); + throw CreateInvalidFormatSpecifierException(); } [Obsolete("The provider argument is not used. Use ToString() instead.")] @@ -1574,9 +1649,8 @@ public string ToString(IFormatProvider? provider) #endregion #region IConvertible - public TypeCode GetTypeCode() - { - return (InternalGetCorElementType()) switch + public TypeCode GetTypeCode() => + InternalGetCorElementType() switch { CorElementType.ELEMENT_TYPE_I1 => TypeCode.SByte, CorElementType.ELEMENT_TYPE_U1 => TypeCode.Byte, @@ -1590,7 +1664,6 @@ public TypeCode GetTypeCode() CorElementType.ELEMENT_TYPE_U8 => TypeCode.UInt64, _ => throw new InvalidOperationException(SR.InvalidOperation_UnknownEnumType), }; - } bool IConvertible.ToBoolean(IFormatProvider? provider) { @@ -1713,13 +1786,22 @@ private static RuntimeType ValidateRuntimeType(Type enumType) throw new ArgumentException(SR.Arg_MustBeType, nameof(enumType)); if (!rtType.IsActualEnum) throw new ArgumentException(SR.Arg_MustBeEnum, nameof(enumType)); + #if NATIVEAOT // Check for the unfortunate "typeof(Outer<>.InnerEnum)" corner case. // https://github.com/dotnet/runtime/issues/7976 if (enumType.ContainsGenericParameters) throw new InvalidOperationException(SR.Format(SR.Arg_OpenType, enumType.ToString())); #endif + return rtType; } + + private static void ThrowInvalidEmptyParseArgument() => + throw new ArgumentException(SR.Arg_MustContainEnumInfo, "value"); + + [MethodImpl(MethodImplOptions.NoInlining)] // https://github.com/dotnet/runtime/issues/78300 + private static Exception CreateInvalidFormatSpecifierException() => + new FormatException(SR.Format_InvalidEnumFormatSpecification); } } diff --git a/src/libraries/System.Runtime/ref/System.Runtime.cs b/src/libraries/System.Runtime/ref/System.Runtime.cs index 47ec3b43aaebe..bd600a29568a5 100644 --- a/src/libraries/System.Runtime/ref/System.Runtime.cs +++ b/src/libraries/System.Runtime/ref/System.Runtime.cs @@ -2366,7 +2366,7 @@ protected Enum() { } public string ToString([System.Diagnostics.CodeAnalysis.StringSyntaxAttribute("EnumFormat")] string? format) { throw null; } [System.ObsoleteAttribute("The provider argument is not used. Use ToString(String) instead.")] public string ToString([System.Diagnostics.CodeAnalysis.StringSyntaxAttribute("EnumFormat")] string? format, System.IFormatProvider? provider) { throw null; } - public static bool TryFormat(TEnum value, System.Span destination, out int charsWritten, [System.Diagnostics.CodeAnalysis.StringSyntaxAttribute("EnumFormat")] System.ReadOnlySpan format) where TEnum : struct { throw null; } + public static bool TryFormat(TEnum value, System.Span destination, out int charsWritten, [System.Diagnostics.CodeAnalysis.StringSyntaxAttribute("EnumFormat")] System.ReadOnlySpan format = default(System.ReadOnlySpan)) where TEnum : struct { throw null; } public static bool TryParse(System.Type enumType, System.ReadOnlySpan value, bool ignoreCase, [System.Diagnostics.CodeAnalysis.NotNullWhenAttribute(true)] out object? result) { throw null; } public static bool TryParse(System.Type enumType, System.ReadOnlySpan value, [System.Diagnostics.CodeAnalysis.NotNullWhenAttribute(true)] out object? result) { throw null; } public static bool TryParse(System.Type enumType, string? value, bool ignoreCase, [System.Diagnostics.CodeAnalysis.NotNullWhenAttribute(true)] out object? result) { throw null; } From 7f3fd23d7a8717d76f1c491d374c7e20b87f9f0e Mon Sep 17 00:00:00 2001 From: Stephen Toub Date: Mon, 14 Nov 2022 22:53:29 -0500 Subject: [PATCH 3/6] More revisions to Enum.TryFormat - Use Enum.GetUnderlyingType instead of Type.GetTypeCode so that the intrinsic enables optimization at the call site - Add a generic cache with readonly statics that enables more generic specialization and branch elimination at call sites - Add more missing nint/nuint/char/etc. cases - Add a generic GetEnumName that can trim away half based on ValuesAreSequentialFromZero, that can get a hardcoded names address (once the frozen work is complete), and that can optimize out a branch in the inlined FindDefinedIndex - Remove a bounds check from GetEnumName on the find path - Expose an internal TryFormatUnconstrained without Enum/struct constraints for corelib interpolated string handlers to use --- .../System.Private.CoreLib/src/System/Enum.cs | 338 +++++++++++------- 1 file changed, 199 insertions(+), 139 deletions(-) diff --git a/src/libraries/System.Private.CoreLib/src/System/Enum.cs b/src/libraries/System.Private.CoreLib/src/System/Enum.cs index fc058799df8f1..fafc7f7791442 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Enum.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Enum.cs @@ -57,56 +57,42 @@ private string AsNumberToString() } [MethodImpl(MethodImplOptions.AggressiveInlining)] // optimizes into a single TryFormat call for all types expressible in C# - private static bool TryAsNumberToString(TEnum value, Span destination, out int charsWritten) where TEnum : struct, Enum + private static bool TryAsNumberToString(TEnum value, Span destination, out int charsWritten) { - switch (Type.GetTypeCode(typeof(TEnum))) - { - case TypeCode.SByte: - return Unsafe.As(ref value).TryFormat(destination, out charsWritten); + Type underlyingType = GetUnderlyingType(typeof(TEnum)); - case TypeCode.Byte: - return Unsafe.As(ref value).TryFormat(destination, out charsWritten); + if (underlyingType == typeof(int)) return Unsafe.As(ref value).TryFormat(destination, out charsWritten); + if (underlyingType == typeof(uint)) return Unsafe.As(ref value).TryFormat(destination, out charsWritten); - case TypeCode.Boolean: - return Unsafe.As(ref value).TryFormat(destination, out charsWritten); + if (underlyingType == typeof(byte)) return Unsafe.As(ref value).TryFormat(destination, out charsWritten); + if (underlyingType == typeof(sbyte)) return Unsafe.As(ref value).TryFormat(destination, out charsWritten); - case TypeCode.Int16: - return Unsafe.As(ref value).TryFormat(destination, out charsWritten); + if (underlyingType == typeof(long)) return Unsafe.As(ref value).TryFormat(destination, out charsWritten); + if (underlyingType == typeof(ulong)) return Unsafe.As(ref value).TryFormat(destination, out charsWritten); - case TypeCode.UInt16: - return Unsafe.As(ref value).TryFormat(destination, out charsWritten); + if (underlyingType == typeof(short)) return Unsafe.As(ref value).TryFormat(destination, out charsWritten); + if (underlyingType == typeof(ushort)) return Unsafe.As(ref value).TryFormat(destination, out charsWritten); - case TypeCode.Char: - if (!destination.IsEmpty) - { - destination[0] = Unsafe.As(ref value); - charsWritten = 1; - return true; - } - charsWritten = 0; - return false; - - case TypeCode.UInt32: - return Unsafe.As(ref value).TryFormat(destination, out charsWritten); - - case TypeCode.Int32: - return Unsafe.As(ref value).TryFormat(destination, out charsWritten); - - case TypeCode.UInt64: - return Unsafe.As(ref value).TryFormat(destination, out charsWritten); - - case TypeCode.Int64: - return Unsafe.As(ref value).TryFormat(destination, out charsWritten); + if (underlyingType == typeof(nint)) return Unsafe.As(ref value).TryFormat(destination, out charsWritten); + if (underlyingType == typeof(nuint)) return Unsafe.As(ref value).TryFormat(destination, out charsWritten); - case TypeCode.Single: - return Unsafe.As(ref value).TryFormat(destination, out charsWritten); + if (underlyingType == typeof(float)) return Unsafe.As(ref value).TryFormat(destination, out charsWritten); + if (underlyingType == typeof(double)) return Unsafe.As(ref value).TryFormat(destination, out charsWritten); - case TypeCode.Double: - return Unsafe.As(ref value).TryFormat(destination, out charsWritten); + if (underlyingType == typeof(bool)) return Unsafe.As(ref value).TryFormat(destination, out charsWritten); + if (underlyingType == typeof(char)) + { + if (!destination.IsEmpty) + { + destination[0] = Unsafe.As(ref value); + charsWritten = 1; + return true; + } + charsWritten = 0; + return false; + } - default: - throw new InvalidOperationException(SR.InvalidOperation_UnknownEnumType); - }; + throw new InvalidOperationException(SR.InvalidOperation_UnknownEnumType); } private string AsNumberToHexString() @@ -152,32 +138,46 @@ private string AsNumberToHexString() } [MethodImpl(MethodImplOptions.AggressiveInlining)] // optimizes into a single TryFormat call for all types expressible in C# - private static bool TryAsNumberToHexString(TEnum value, Span destination, out int charsWritten) where TEnum : struct, Enum + private static bool TryAsNumberToHexString(TEnum value, Span destination, out int charsWritten) { - switch (Type.GetTypeCode(typeof(TEnum))) + Type underlyingType = GetUnderlyingType(typeof(TEnum)); + + if (underlyingType == typeof(int) || underlyingType == typeof(uint) +#if TARGET_32BIT + || underlyingType == typeof(nint) || underlyingType == typeof(nuint) +#endif + ) { - case TypeCode.Byte or TypeCode.SByte: - return Unsafe.As(ref value).TryFormat(destination, out charsWritten, "X2"); + return Unsafe.As(ref value).TryFormat(destination, out charsWritten, "X8"); + } - case TypeCode.Boolean: - { - bool copied = (Unsafe.As(ref value) ? "01" : "00").TryCopyTo(destination); - charsWritten = copied ? 2 : 0; - return copied; - } + if (underlyingType == typeof(byte) || underlyingType == typeof(sbyte)) + { + return Unsafe.As(ref value).TryFormat(destination, out charsWritten, "X2"); + } - case TypeCode.UInt16 or TypeCode.Int16 or TypeCode.Char: - return Unsafe.As(ref value).TryFormat(destination, out charsWritten, "X4"); + if (underlyingType == typeof(long) || underlyingType == typeof(ulong) +#if TARGET_64BIT + || underlyingType == typeof(nint) || underlyingType == typeof(nuint) +#endif + ) + { + return Unsafe.As(ref value).TryFormat(destination, out charsWritten, "X16"); + } - case TypeCode.UInt32 or TypeCode.Int32: - return Unsafe.As(ref value).TryFormat(destination, out charsWritten, "X8"); + if (underlyingType == typeof(short) || underlyingType == typeof(ushort) || underlyingType == typeof(char)) + { + return Unsafe.As(ref value).TryFormat(destination, out charsWritten, "X4"); + } - case TypeCode.UInt64 or TypeCode.Int64: - return Unsafe.As(ref value).TryFormat(destination, out charsWritten, "X16"); + if (underlyingType == typeof(bool)) + { + bool copied = (Unsafe.As(ref value) ? "01" : "00").TryCopyTo(destination); + charsWritten = copied ? 2 : 0; + return copied; + } - default: - throw new InvalidOperationException(SR.InvalidOperation_UnknownEnumType); - }; + throw new InvalidOperationException(SR.InvalidOperation_UnknownEnumType); } private static string AsNumberToHexString(object value) => @@ -196,17 +196,15 @@ private static string AsNumberToHexString(object value) => _ => throw new InvalidOperationException(SR.InvalidOperation_UnknownEnumType), }; - internal static string? GetEnumName(RuntimeType enumType, ulong ulValue) - { - return GetEnumName(GetEnumInfo(enumType), ulValue); - } + internal static string? GetEnumName(RuntimeType enumType, ulong ulValue) => + GetEnumName(GetEnumInfo(enumType), ulValue); [MethodImpl(MethodImplOptions.AggressiveInlining)] private static string? GetEnumName(EnumInfo enumInfo, ulong ulValue) { + string[] names = enumInfo.Names; if (enumInfo.ValuesAreSequentialFromZero) { - string[] names = enumInfo.Names; if (ulValue < (ulong)names.Length) { return names[(uint)ulValue]; @@ -215,7 +213,7 @@ private static string AsNumberToHexString(object value) => else { int index = FindDefinedIndex(enumInfo.Values, ulValue); - if (index >= 0) + if ((uint)index < (uint)names.Length) { return enumInfo.Names[index]; } @@ -224,6 +222,29 @@ private static string AsNumberToHexString(object value) => return null; // return null so the caller knows to .ToString() the input } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static string? GetEnumName(ulong ulValue) + { + string[] names = GenericEnumInfo.Names; + if (GenericEnumInfo.ValuesAreSequentialFromZero) + { + if (ulValue < (ulong)names.Length) + { + return names[(uint)ulValue]; + } + } + else + { + int index = FindDefinedIndex(GenericEnumInfo.Values, ulValue); + if ((uint)index < (uint)names.Length) + { + return names[index]; + } + } + + return null; // return null so the caller knows to .ToString() the input + } + private static string? FormatSingleNameOrFlagNames(RuntimeType enumType, ulong value) { EnumInfo enumInfo = GetEnumInfo(enumType); @@ -432,26 +453,35 @@ internal static ulong ToUInt64(object value) => _ => throw new InvalidOperationException(SR.InvalidOperation_UnknownEnumType), }; - private static ulong ToUInt64(TEnum value) where TEnum : struct, Enum => - Type.GetTypeCode(typeof(TEnum)) switch - { - TypeCode.SByte => (ulong)Unsafe.As(ref value), - TypeCode.Byte => Unsafe.As(ref value), - TypeCode.Boolean => Unsafe.As(ref value) ? 1UL : 0UL, - TypeCode.Int16 => (ulong)Unsafe.As(ref value), - TypeCode.UInt16 => Unsafe.As(ref value), - TypeCode.Char => Unsafe.As(ref value), - TypeCode.UInt32 => Unsafe.As(ref value), - TypeCode.Int32 => (ulong)Unsafe.As(ref value), - TypeCode.UInt64 => Unsafe.As(ref value), - TypeCode.Int64 => (ulong)Unsafe.As(ref value), - _ => throw new InvalidOperationException(SR.InvalidOperation_UnknownEnumType), - }; - #endregion + private static ulong ToUInt64(TEnum value) + { + Type underlyingType = GetUnderlyingType(typeof(TEnum)); + + if (underlyingType == typeof(int)) return (ulong)Unsafe.As(ref value); + if (underlyingType == typeof(uint)) return Unsafe.As(ref value); + + if (underlyingType == typeof(byte)) return Unsafe.As(ref value); + if (underlyingType == typeof(sbyte)) return (ulong)Unsafe.As(ref value); + + if (underlyingType == typeof(long)) return (ulong)Unsafe.As(ref value); + if (underlyingType == typeof(ulong)) return Unsafe.As(ref value); + + if (underlyingType == typeof(short)) return (ulong)Unsafe.As(ref value); + if (underlyingType == typeof(ushort)) return Unsafe.As(ref value); + + if (underlyingType == typeof(nint)) return (ulong)Unsafe.As(ref value); + if (underlyingType == typeof(nuint)) return Unsafe.As(ref value); + + if (underlyingType == typeof(bool)) return Unsafe.As(ref value) ? 1UL : 0UL; + if (underlyingType == typeof(char)) return Unsafe.As(ref value); + + throw new InvalidOperationException(SR.InvalidOperation_UnknownEnumType); + } +#endregion #region Public Static Methods public static string? GetName(TEnum value) where TEnum : struct, Enum => - GetEnumName((RuntimeType)typeof(TEnum), ToUInt64(value)); + GetEnumName(ToUInt64(value)); public static string? GetName(Type enumType, object value) { @@ -460,7 +490,7 @@ private static ulong ToUInt64(TEnum value) where TEnum : struct, Enum => } public static string[] GetNames() where TEnum : struct, Enum => - new ReadOnlySpan(InternalGetNames((RuntimeType)typeof(TEnum))).ToArray(); + new ReadOnlySpan(GenericEnumInfo.Names).ToArray(); public static string[] GetNames(Type enumType) { @@ -588,16 +618,12 @@ internal static ulong[] InternalGetValues(RuntimeType enumType) public static bool IsDefined(TEnum value) where TEnum : struct, Enum { - RuntimeType enumType = (RuntimeType)typeof(TEnum); - EnumInfo info = GetEnumInfo(enumType, getNames: false); - ulong ulValue = ToUInt64(value); - ulong[] ulValues = info.Values; - // If the enum's values are all sequentially numbered starting from 0, then we can // just return if the requested index is in range. Otherwise, search for the value. + ulong ulValue = ToUInt64(value); return - info.ValuesAreSequentialFromZero ? ulValue < (ulong)ulValues.Length : - FindDefinedIndex(ulValues, ulValue) >= 0; + GenericEnumInfo.ValuesAreSequentialFromZero ? ulValue < (ulong)GenericEnumInfo.Values.Length : + FindDefinedIndex(GenericEnumInfo.Values, ulValue) >= 0; } [MethodImpl(MethodImplOptions.AggressiveInlining)] @@ -888,57 +914,71 @@ private static bool TryParse(ReadOnlySpan value, bool ignoreCase, b bool parsed; RuntimeType rt = (RuntimeType)typeof(TEnum); - switch (Type.GetTypeCode(typeof(TEnum))) - { - case TypeCode.SByte: - parsed = TryParseInt32Enum(rt, value, sbyte.MinValue, sbyte.MaxValue, ignoreCase, throwOnFailure, TypeCode.SByte, out intResult); - sbyte sbyteResult = (sbyte)intResult; - result = Unsafe.As(ref sbyteResult); - return parsed; + Type underlyingType = GetUnderlyingType(typeof(TEnum)); - case TypeCode.Int16: - parsed = TryParseInt32Enum(rt, value, short.MinValue, short.MaxValue, ignoreCase, throwOnFailure, TypeCode.Int16, out intResult); - short shortResult = (short)intResult; - result = Unsafe.As(ref shortResult); - return parsed; + if (underlyingType == typeof(int)) + { + parsed = TryParseInt32Enum(rt, value, int.MinValue, int.MaxValue, ignoreCase, throwOnFailure, TypeCode.Int32, out intResult); + result = Unsafe.As(ref intResult); + return parsed; + } - case TypeCode.Int32: - parsed = TryParseInt32Enum(rt, value, int.MinValue, int.MaxValue, ignoreCase, throwOnFailure, TypeCode.Int32, out intResult); - result = Unsafe.As(ref intResult); - return parsed; + if (underlyingType == typeof(uint)) + { + parsed = TryParseUInt32Enum(rt, value, uint.MaxValue, ignoreCase, throwOnFailure, TypeCode.UInt32, out uintResult); + result = Unsafe.As(ref uintResult); + return parsed; + } - case TypeCode.Byte: - parsed = TryParseUInt32Enum(rt, value, byte.MaxValue, ignoreCase, throwOnFailure, TypeCode.Byte, out uintResult); - byte byteResult = (byte)uintResult; - result = Unsafe.As(ref byteResult); - return parsed; + if (underlyingType == typeof(byte)) + { + parsed = TryParseUInt32Enum(rt, value, byte.MaxValue, ignoreCase, throwOnFailure, TypeCode.Byte, out uintResult); + byte byteResult = (byte)uintResult; + result = Unsafe.As(ref byteResult); + return parsed; + } - case TypeCode.UInt16: - parsed = TryParseUInt32Enum(rt, value, ushort.MaxValue, ignoreCase, throwOnFailure, TypeCode.UInt16, out uintResult); - ushort ushortResult = (ushort)uintResult; - result = Unsafe.As(ref ushortResult); - return parsed; + if (underlyingType == typeof(sbyte)) + { + parsed = TryParseInt32Enum(rt, value, sbyte.MinValue, sbyte.MaxValue, ignoreCase, throwOnFailure, TypeCode.SByte, out intResult); + sbyte sbyteResult = (sbyte)intResult; + result = Unsafe.As(ref sbyteResult); + return parsed; + } - case TypeCode.UInt32: - parsed = TryParseUInt32Enum(rt, value, uint.MaxValue, ignoreCase, throwOnFailure, TypeCode.UInt32, out uintResult); - result = Unsafe.As(ref uintResult); - return parsed; + if (underlyingType == typeof(long)) + { + parsed = TryParseInt64Enum(rt, value, ignoreCase, throwOnFailure, out long longResult); + result = Unsafe.As(ref longResult); + return parsed; + } - case TypeCode.Int64: - parsed = TryParseInt64Enum(rt, value, ignoreCase, throwOnFailure, out long longResult); - result = Unsafe.As(ref longResult); - return parsed; + if (underlyingType == typeof(ulong)) + { + parsed = TryParseUInt64Enum(rt, value, ignoreCase, throwOnFailure, out ulong ulongResult); + result = Unsafe.As(ref ulongResult); + return parsed; + } - case TypeCode.UInt64: - parsed = TryParseUInt64Enum(rt, value, ignoreCase, throwOnFailure, out ulong ulongResult); - result = Unsafe.As(ref ulongResult); - return parsed; + if (underlyingType == typeof(short)) + { + parsed = TryParseInt32Enum(rt, value, short.MinValue, short.MaxValue, ignoreCase, throwOnFailure, TypeCode.Int16, out intResult); + short shortResult = (short)intResult; + result = Unsafe.As(ref shortResult); + return parsed; + } - default: - parsed = TryParseRareEnum(rt, value, ignoreCase, throwOnFailure, out object? objectResult); - result = parsed ? (TEnum)objectResult! : default; - return parsed; + if (underlyingType == typeof(ushort)) + { + parsed = TryParseUInt32Enum(rt, value, ushort.MaxValue, ignoreCase, throwOnFailure, TypeCode.UInt16, out uintResult); + ushort ushortResult = (ushort)uintResult; + result = Unsafe.As(ref ushortResult); + return parsed; } + + parsed = TryParseRareEnum(rt, value, ignoreCase, throwOnFailure, out object? objectResult); + result = parsed ? (TEnum)objectResult! : default; + return parsed; } /// Tries to parse the value of an enum with known underlying types that fit in an Int32 (Int32, Int16, and SByte). @@ -1301,21 +1341,29 @@ public static string Format(Type enumType, object value, [StringSyntax(StringSyn /// A span containing the character that represents the standard format string that defines the acceptable format of destination. This may be empty, or "g", "d", "f", or "x". /// if the formatting was successful; otherwise, if the destination span wasn't large enough to contain the formatted value. /// The format parameter contains an invalid value. + public static bool TryFormat(TEnum value, Span destination, out int charsWritten, [StringSyntax(StringSyntaxAttribute.EnumFormat)] ReadOnlySpan format = default) where TEnum : struct, Enum => + TryFormatUnconstrained(value, destination, out charsWritten, format); + [MethodImpl(MethodImplOptions.AggressiveInlining)] // format is most frequently a constant, and we want it exposed to the implementation; this should be inlined automatically, anyway - public static bool TryFormat(TEnum value, Span destination, out int charsWritten, [StringSyntax(StringSyntaxAttribute.EnumFormat)] ReadOnlySpan format = default) where TEnum : struct, Enum + internal static bool TryFormatUnconstrained(TEnum value, Span destination, out int charsWritten, [StringSyntax(StringSyntaxAttribute.EnumFormat)] ReadOnlySpan format = default) { + Debug.Assert(typeof(TEnum).IsEnum); + Debug.Assert(value is not null); + return format.IsEmpty ? TryFormatDefault(value, destination, out charsWritten) : TryFormatNonDefault(value, destination, out charsWritten, format); static bool TryFormatDefault(TEnum value, Span destination, out int charsWritten) { - EnumInfo enumInfo = GetEnumInfo((RuntimeType)typeof(TEnum)); + Debug.Assert(typeof(TEnum).IsEnum); + Debug.Assert(value is not null); + ulong ulongValue = ToUInt64(value); - if (!enumInfo.HasFlagsAttribute) + if (!GenericEnumInfo.HasFlagsAttribute) { - if (GetEnumName(enumInfo, ulongValue) is string enumName) + if (GetEnumName(ulongValue) is string enumName) { if (enumName.TryCopyTo(destination)) { @@ -1330,7 +1378,7 @@ static bool TryFormatDefault(TEnum value, Span destination, out int charsW else { bool destinationIsTooSmall = false; - if (TryFormatFlagNames(enumInfo, ulongValue, destination, out charsWritten, ref destinationIsTooSmall) || destinationIsTooSmall) + if (TryFormatFlagNames(GenericEnumInfo.EnumInfo, ulongValue, destination, out charsWritten, ref destinationIsTooSmall) || destinationIsTooSmall) { return !destinationIsTooSmall; } @@ -1341,6 +1389,9 @@ static bool TryFormatDefault(TEnum value, Span destination, out int charsW static bool TryFormatNonDefault(TEnum value, Span destination, out int charsWritten, ReadOnlySpan format) { + Debug.Assert(typeof(TEnum).IsEnum); + Debug.Assert(value is not null); + if (format.Length == 1) { switch (format[0] | 0x20) @@ -1356,7 +1407,7 @@ static bool TryFormatNonDefault(TEnum value, Span destination, out int cha case 'f': bool destinationIsTooSmall = false; - if (TryFormatFlagNames(GetEnumInfo((RuntimeType)typeof(TEnum)), ToUInt64(value), destination, out charsWritten, ref destinationIsTooSmall) || + if (TryFormatFlagNames(GenericEnumInfo.EnumInfo, ToUInt64(value), destination, out charsWritten, ref destinationIsTooSmall) || destinationIsTooSmall) { return !destinationIsTooSmall; @@ -1803,5 +1854,14 @@ private static void ThrowInvalidEmptyParseArgument() => [MethodImpl(MethodImplOptions.NoInlining)] // https://github.com/dotnet/runtime/issues/78300 private static Exception CreateInvalidFormatSpecifierException() => new FormatException(SR.Format_InvalidEnumFormatSpecification); + + private static class GenericEnumInfo + { + public static readonly EnumInfo EnumInfo = GetEnumInfo((RuntimeType)typeof(TEnum)); + public static readonly bool HasFlagsAttribute = EnumInfo.HasFlagsAttribute; + public static readonly string[] Names = EnumInfo.Names; + public static readonly ulong[] Values = EnumInfo.Values; + public static readonly bool ValuesAreSequentialFromZero = EnumInfo.ValuesAreSequentialFromZero; + } } } From 89f84beca1fecf3bb7702703b617dc3925077c89 Mon Sep 17 00:00:00 2001 From: Stephen Toub Date: Mon, 14 Nov 2022 22:41:49 -0500 Subject: [PATCH 4/6] Use Enum.TryFormat in interpolated string handlers --- .../src/System/MemoryExtensions.cs | 29 ++++++++++++++++--- .../DefaultInterpolatedStringHandler.cs | 26 +++++++++++++++++ .../src/System/Text/StringBuilder.cs | 22 ++++++++++++++ 3 files changed, 73 insertions(+), 4 deletions(-) diff --git a/src/libraries/System.Private.CoreLib/src/System/MemoryExtensions.cs b/src/libraries/System.Private.CoreLib/src/System/MemoryExtensions.cs index 9bb51d939f255..62e480ca704b1 100644 --- a/src/libraries/System.Private.CoreLib/src/System/MemoryExtensions.cs +++ b/src/libraries/System.Private.CoreLib/src/System/MemoryExtensions.cs @@ -3271,10 +3271,21 @@ public bool AppendFormatted(T value) if (value is IFormattable) { // If the value can format itself directly into our buffer, do so. + if (value is ISpanFormattable) { - int charsWritten; - if (((ISpanFormattable)value).TryFormat(_destination.Slice(_pos), out charsWritten, default, _provider)) // constrained call avoiding boxing for value types + if (((ISpanFormattable)value).TryFormat(_destination.Slice(_pos), out int charsWritten, default, _provider)) // constrained call avoiding boxing for value types + { + _pos += charsWritten; + return true; + } + + return Fail(); + } + + if (typeof(T).IsEnum) + { + if (Enum.TryFormatUnconstrained(value, _destination.Slice(_pos), out int charsWritten)) { _pos += charsWritten; return true; @@ -3318,8 +3329,18 @@ public bool AppendFormatted(T value, string? format) // If the value can format itself directly into our buffer, do so. if (value is ISpanFormattable) { - int charsWritten; - if (((ISpanFormattable)value).TryFormat(_destination.Slice(_pos), out charsWritten, format, _provider)) // constrained call avoiding boxing for value types + if (((ISpanFormattable)value).TryFormat(_destination.Slice(_pos), out int charsWritten, format, _provider)) // constrained call avoiding boxing for value types + { + _pos += charsWritten; + return true; + } + + return Fail(); + } + + if (typeof(T).IsEnum) + { + if (Enum.TryFormatUnconstrained(value, _destination.Slice(_pos), out int charsWritten, format)) { _pos += charsWritten; return true; diff --git a/src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/DefaultInterpolatedStringHandler.cs b/src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/DefaultInterpolatedStringHandler.cs index 053528116e6ca..334fae3c3506d 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/DefaultInterpolatedStringHandler.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/DefaultInterpolatedStringHandler.cs @@ -305,6 +305,7 @@ public void AppendFormatted(T value) if (value is IFormattable) { // If the value can format itself directly into our buffer, do so. + if (value is ISpanFormattable) { int charsWritten; @@ -317,6 +318,18 @@ public void AppendFormatted(T value) return; } + if (typeof(T).IsEnum) + { + int charsWritten; + while (!Enum.TryFormatUnconstrained(value, _chars.Slice(_pos), out charsWritten)) + { + Grow(); + } + + _pos += charsWritten; + return; + } + s = ((IFormattable)value).ToString(format: null, _provider); // constrained call avoiding boxing for value types } else @@ -329,6 +342,7 @@ public void AppendFormatted(T value) AppendStringDirect(s); } } + /// Writes the specified value to the handler. /// The value to write. /// The format string. @@ -365,6 +379,18 @@ public void AppendFormatted(T value, string? format) return; } + if (typeof(T).IsEnum) + { + int charsWritten; + while (!Enum.TryFormatUnconstrained(value, _chars.Slice(_pos), out charsWritten, format)) + { + Grow(); + } + + _pos += charsWritten; + return; + } + s = ((IFormattable)value).ToString(format, _provider); // constrained call avoiding boxing for value types } else diff --git a/src/libraries/System.Private.CoreLib/src/System/Text/StringBuilder.cs b/src/libraries/System.Private.CoreLib/src/System/Text/StringBuilder.cs index 65a228fa5fea4..ad977792a9111 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Text/StringBuilder.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Text/StringBuilder.cs @@ -2640,6 +2640,17 @@ public void AppendFormatted(T value) AppendFormattedWithTempSpace(value, 0, format: null); } } + else if (typeof(T).IsEnum) + { + if (Enum.TryFormatUnconstrained(value, _stringBuilder.RemainingCurrentChunk, out int charsWritten)) + { + _stringBuilder.m_ChunkLength += charsWritten; + } + else + { + AppendFormattedWithTempSpace(value, 0, format: null); + } + } else { _stringBuilder.Append(((IFormattable)value).ToString(format: null, _provider)); // constrained call avoiding boxing for value types @@ -2693,6 +2704,17 @@ public void AppendFormatted(T value, string? format) AppendFormattedWithTempSpace(value, 0, format); } } + else if (typeof(T).IsEnum) + { + if (Enum.TryFormatUnconstrained(value, _stringBuilder.RemainingCurrentChunk, out int charsWritten, format)) + { + _stringBuilder.m_ChunkLength += charsWritten; + } + else + { + AppendFormattedWithTempSpace(value, 0, format); + } + } else { _stringBuilder.Append(((IFormattable)value).ToString(format, _provider)); // constrained call avoiding boxing for value types From 7b05542c75337d2f558ea4dc30001f56b3f40fd3 Mon Sep 17 00:00:00 2001 From: Stephen Toub Date: Mon, 14 Nov 2022 22:39:14 -0500 Subject: [PATCH 5/6] Use generic Enum.IsDefined in a few more places --- .../Microsoft/CSharp/RuntimeBinder/Semantics/Operators.cs | 6 +++--- .../src/Microsoft/Win32/RegistryKey.cs | 2 +- .../src/System/Diagnostics/Process.cs | 2 +- .../src/System/Diagnostics/ProcessStartInfo.cs | 2 +- .../src/System/Drawing/Printing/PrinterSettings.cs | 2 +- .../System.Private.CoreLib/src/System/Environment.cs | 4 ++-- .../System/Resources/NeutralResourcesLanguageAttribute.cs | 2 +- .../System.Private.CoreLib/src/System/ThrowHelper.cs | 4 ++-- src/libraries/System.Private.Uri/src/System/UriExt.cs | 4 ---- .../src/System/Xml/Xsl/IlGen/OptimizerPatterns.cs | 4 ++-- .../src/System/ServiceProcess/ServiceController.cs | 2 +- 11 files changed, 15 insertions(+), 19 deletions(-) diff --git a/src/libraries/Microsoft.CSharp/src/Microsoft/CSharp/RuntimeBinder/Semantics/Operators.cs b/src/libraries/Microsoft.CSharp/src/Microsoft/CSharp/RuntimeBinder/Semantics/Operators.cs index 6a16f18b34534..97f2815c7f379 100644 --- a/src/libraries/Microsoft.CSharp/src/Microsoft/CSharp/RuntimeBinder/Semantics/Operators.cs +++ b/src/libraries/Microsoft.CSharp/src/Microsoft/CSharp/RuntimeBinder/Semantics/Operators.cs @@ -1046,8 +1046,8 @@ private int WhichBofsIsBetter(BinOpFullSig bofs1, BinOpFullSig bofs2, CType type bt2 = WhichTypeIsBetter(bofs1.Type2(), bofs2.Type2(), type2); } - Debug.Assert(Enum.IsDefined(typeof(BetterType), bt1)); - Debug.Assert(Enum.IsDefined(typeof(BetterType), bt2)); + Debug.Assert(Enum.IsDefined(bt1)); + Debug.Assert(Enum.IsDefined(bt2)); int res = bt1 switch { BetterType.Left => -1, @@ -1550,7 +1550,7 @@ private int WhichUofsIsBetter(UnaOpFullSig uofs1, UnaOpFullSig uofs2, CType type bt = WhichTypeIsBetter(uofs1.GetType(), uofs2.GetType(), typeArg); } - Debug.Assert(Enum.IsDefined(typeof(BetterType), bt)); + Debug.Assert(Enum.IsDefined(bt)); return bt switch { BetterType.Left => -1, diff --git a/src/libraries/Microsoft.Win32.Registry/src/Microsoft/Win32/RegistryKey.cs b/src/libraries/Microsoft.Win32.Registry/src/Microsoft/Win32/RegistryKey.cs index 2d6f85015a8c4..fd6db1f9628b3 100644 --- a/src/libraries/Microsoft.Win32.Registry/src/Microsoft/Win32/RegistryKey.cs +++ b/src/libraries/Microsoft.Win32.Registry/src/Microsoft/Win32/RegistryKey.cs @@ -565,7 +565,7 @@ public void SetValue(string? name, object value, RegistryValueKind valueKind) throw new ArgumentException(SR.Arg_RegValStrLenBug, nameof(name)); } - if (!Enum.IsDefined(typeof(RegistryValueKind), valueKind)) + if (!Enum.IsDefined(valueKind)) { throw new ArgumentException(SR.Arg_RegBadKeyKind, nameof(valueKind)); } diff --git a/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.cs b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.cs index d1e5d6f939d81..8f2500db732c7 100644 --- a/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.cs +++ b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.cs @@ -480,7 +480,7 @@ public ProcessPriorityClass PriorityClass } set { - if (!Enum.IsDefined(typeof(ProcessPriorityClass), value)) + if (!Enum.IsDefined(value)) { throw new InvalidEnumArgumentException(nameof(value), (int)value, typeof(ProcessPriorityClass)); } diff --git a/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ProcessStartInfo.cs b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ProcessStartInfo.cs index 761f1cd3b46d0..b2941f17dfd5a 100644 --- a/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ProcessStartInfo.cs +++ b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ProcessStartInfo.cs @@ -161,7 +161,7 @@ public ProcessWindowStyle WindowStyle get => _windowStyle; set { - if (!Enum.IsDefined(typeof(ProcessWindowStyle), value)) + if (!Enum.IsDefined(value)) { throw new InvalidEnumArgumentException(nameof(value), (int)value, typeof(ProcessWindowStyle)); } diff --git a/src/libraries/System.Drawing.Common/src/System/Drawing/Printing/PrinterSettings.cs b/src/libraries/System.Drawing.Common/src/System/Drawing/Printing/PrinterSettings.cs index 55a880becb59e..441809b55b13c 100644 --- a/src/libraries/System.Drawing.Common/src/System/Drawing/Printing/PrinterSettings.cs +++ b/src/libraries/System.Drawing.Common/src/System/Drawing/Printing/PrinterSettings.cs @@ -371,7 +371,7 @@ public PrintRange PrintRange get { return _printRange; } set { - if (!Enum.IsDefined(typeof(PrintRange), value)) + if (!Enum.IsDefined(value)) throw new InvalidEnumArgumentException(nameof(value), unchecked((int)value), typeof(PrintRange)); _printRange = value; diff --git a/src/libraries/System.Private.CoreLib/src/System/Environment.cs b/src/libraries/System.Private.CoreLib/src/System/Environment.cs index cc67fc169bced..99956b82abf55 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Environment.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Environment.cs @@ -112,10 +112,10 @@ public static string ExpandEnvironmentVariables(string name) public static string GetFolderPath(SpecialFolder folder, SpecialFolderOption option) { - if (!Enum.IsDefined(typeof(SpecialFolder), folder)) + if (!Enum.IsDefined(folder)) throw new ArgumentOutOfRangeException(nameof(folder), folder, SR.Format(SR.Arg_EnumIllegalVal, folder)); - if (option != SpecialFolderOption.None && !Enum.IsDefined(typeof(SpecialFolderOption), option)) + if (option != SpecialFolderOption.None && !Enum.IsDefined(option)) throw new ArgumentOutOfRangeException(nameof(option), option, SR.Format(SR.Arg_EnumIllegalVal, option)); return GetFolderPathCore(folder, option); diff --git a/src/libraries/System.Private.CoreLib/src/System/Resources/NeutralResourcesLanguageAttribute.cs b/src/libraries/System.Private.CoreLib/src/System/Resources/NeutralResourcesLanguageAttribute.cs index ceed4e4133523..d912df64de133 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Resources/NeutralResourcesLanguageAttribute.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Resources/NeutralResourcesLanguageAttribute.cs @@ -21,7 +21,7 @@ public NeutralResourcesLanguageAttribute(string cultureName, UltimateResourceFal { ArgumentNullException.ThrowIfNull(cultureName); - if (!Enum.IsDefined(typeof(UltimateResourceFallbackLocation), location)) + if (!Enum.IsDefined(location)) throw new ArgumentException(SR.Format(SR.Arg_InvalidNeutralResourcesLanguage_FallbackLoc, location)); CultureName = cultureName; diff --git a/src/libraries/System.Private.CoreLib/src/System/ThrowHelper.cs b/src/libraries/System.Private.CoreLib/src/System/ThrowHelper.cs index 1f0e03112c8f2..55dda55d8e5bf 100644 --- a/src/libraries/System.Private.CoreLib/src/System/ThrowHelper.cs +++ b/src/libraries/System.Private.CoreLib/src/System/ThrowHelper.cs @@ -692,7 +692,7 @@ internal static void ThrowForUnsupportedIntrinsicsVector256BaseType() [MethodImpl(MethodImplOptions.NoInlining)] private static string GetArgumentName(ExceptionArgument argument) { - Debug.Assert(Enum.IsDefined(typeof(ExceptionArgument), argument), + Debug.Assert(Enum.IsDefined(argument), "The enum value is not defined, please check the ExceptionArgument Enum."); return argument.ToString(); @@ -912,7 +912,7 @@ private static string GetArgumentName(ExceptionArgument argument) [MethodImpl(MethodImplOptions.NoInlining)] private static string GetResourceString(ExceptionResource resource) { - Debug.Assert(Enum.IsDefined(typeof(ExceptionResource), resource), + Debug.Assert(Enum.IsDefined(resource), "The enum value is not defined, please check the ExceptionResource Enum."); return SR.GetResourceString(resource.ToString()); diff --git a/src/libraries/System.Private.Uri/src/System/UriExt.cs b/src/libraries/System.Private.Uri/src/System/UriExt.cs index 8acfa9fff6f5c..14c23bc31c9c6 100644 --- a/src/libraries/System.Private.Uri/src/System/UriExt.cs +++ b/src/libraries/System.Private.Uri/src/System/UriExt.cs @@ -17,8 +17,6 @@ private void CreateThis(string? uri, bool dontEscape, UriKind uriKind, in UriCre { DebugAssertInCtor(); - // if (!Enum.IsDefined(typeof(UriKind), uriKind)) -- We currently believe that Enum.IsDefined() is too slow - // to be used here. if ((int)uriKind < (int)UriKind.RelativeOrAbsolute || (int)uriKind > (int)UriKind.Relative) { throw new ArgumentException(SR.Format(SR.net_uri_InvalidUriKind, uriKind)); @@ -622,8 +620,6 @@ private Uri(Flags flags, UriParser? uriParser, string uri) // internal static Uri? CreateHelper(string uriString, bool dontEscape, UriKind uriKind, ref UriFormatException? e, in UriCreationOptions creationOptions = default) { - // if (!Enum.IsDefined(typeof(UriKind), uriKind)) -- We currently believe that Enum.IsDefined() is too slow - // to be used here. if ((int)uriKind < (int)UriKind.RelativeOrAbsolute || (int)uriKind > (int)UriKind.Relative) { throw new ArgumentException(SR.Format(SR.net_uri_InvalidUriKind, uriKind)); diff --git a/src/libraries/System.Private.Xml/src/System/Xml/Xsl/IlGen/OptimizerPatterns.cs b/src/libraries/System.Private.Xml/src/System/Xml/Xsl/IlGen/OptimizerPatterns.cs index c02d1c707b160..ef53d2f1e6db9 100644 --- a/src/libraries/System.Private.Xml/src/System/Xml/Xsl/IlGen/OptimizerPatterns.cs +++ b/src/libraries/System.Private.Xml/src/System/Xml/Xsl/IlGen/OptimizerPatterns.cs @@ -242,7 +242,7 @@ public object GetArgument(OptimizerPatternArgument argNum) /// public void AddPattern(OptimizerPatternName pattern) { - Debug.Assert(Enum.IsDefined(typeof(OptimizerPatternName), pattern)); + Debug.Assert(Enum.IsDefined(pattern)); Debug.Assert((int)pattern < 32); Debug.Assert(!_isReadOnly, "This OptimizerPatterns instance is read-only."); _patterns |= (1 << (int)pattern); @@ -253,7 +253,7 @@ public void AddPattern(OptimizerPatternName pattern) /// public bool MatchesPattern(OptimizerPatternName pattern) { - Debug.Assert(Enum.IsDefined(typeof(OptimizerPatternName), pattern)); + Debug.Assert(Enum.IsDefined(pattern)); return (_patterns & (1 << (int)pattern)) != 0; } diff --git a/src/libraries/System.ServiceProcess.ServiceController/src/System/ServiceProcess/ServiceController.cs b/src/libraries/System.ServiceProcess.ServiceController/src/System/ServiceProcess/ServiceController.cs index 91ce969aa61af..9f240dead871b 100644 --- a/src/libraries/System.ServiceProcess.ServiceController/src/System/ServiceProcess/ServiceController.cs +++ b/src/libraries/System.ServiceProcess.ServiceController/src/System/ServiceProcess/ServiceController.cs @@ -977,7 +977,7 @@ public void WaitForStatus(ServiceControllerStatus desiredStatus) /// Wait for specific timeout public void WaitForStatus(ServiceControllerStatus desiredStatus, TimeSpan timeout) { - if (!Enum.IsDefined(typeof(ServiceControllerStatus), desiredStatus)) + if (!Enum.IsDefined(desiredStatus)) throw new ArgumentException(SR.Format(SR.InvalidEnumArgument, nameof(desiredStatus), (int)desiredStatus, typeof(ServiceControllerStatus))); DateTime start = DateTime.UtcNow; From f9930368a39103b1292459404e9a26908e9afae8 Mon Sep 17 00:00:00 2001 From: Stephen Toub Date: Tue, 15 Nov 2022 00:03:06 -0500 Subject: [PATCH 6/6] Fix Enum tests --- .../System.Runtime/tests/System/EnumTests.cs | 121 +++++++++--------- 1 file changed, 64 insertions(+), 57 deletions(-) diff --git a/src/libraries/System.Runtime/tests/System/EnumTests.cs b/src/libraries/System.Runtime/tests/System/EnumTests.cs index 4104f4711c917..d49d782786eb1 100644 --- a/src/libraries/System.Runtime/tests/System/EnumTests.cs +++ b/src/libraries/System.Runtime/tests/System/EnumTests.cs @@ -2179,54 +2179,57 @@ public static void ToString_TryFormat(bool validateDestinationSpanSizeCheck, boo // "F": Flags Attribute TryFormat(AttributeTargets.Class | AttributeTargets.Delegate, "F", "Class, Delegate", validateDestinationSpanSizeCheck, validateExtraSpanSpaceNotFilled); - // "G": SByte - TryFormat(SByteEnum.Min, "G", "Min", validateDestinationSpanSizeCheck, validateExtraSpanSpaceNotFilled); - TryFormat((SByteEnum)3, "G", "3", validateDestinationSpanSizeCheck, validateExtraSpanSpaceNotFilled); // No [Flags] attribute - TryFormat(SByteEnum.Max, "G", "Max", validateDestinationSpanSizeCheck, validateExtraSpanSpaceNotFilled); - - // "G": Byte - TryFormat(ByteEnum.Min, "G", "Min", validateDestinationSpanSizeCheck, validateExtraSpanSpaceNotFilled); - TryFormat((ByteEnum)0xff, "G", "Max", validateDestinationSpanSizeCheck, validateExtraSpanSpaceNotFilled); - TryFormat((ByteEnum)3, "G", "3", validateDestinationSpanSizeCheck, validateExtraSpanSpaceNotFilled); // No [Flags] attribute - TryFormat(ByteEnum.Max, "G", "Max", validateDestinationSpanSizeCheck, validateExtraSpanSpaceNotFilled); - - // "G": Int16 - TryFormat(Int16Enum.Min, "G", "Min", validateDestinationSpanSizeCheck, validateExtraSpanSpaceNotFilled); - TryFormat((Int16Enum)3, "G", "3", validateDestinationSpanSizeCheck, validateExtraSpanSpaceNotFilled); // No [Flags] attribute - TryFormat(Int16Enum.Max, "G", "Max", validateDestinationSpanSizeCheck, validateExtraSpanSpaceNotFilled); - - // "G": UInt16 - TryFormat(UInt16Enum.Min, "G", "Min", validateDestinationSpanSizeCheck, validateExtraSpanSpaceNotFilled); - TryFormat((UInt16Enum)3, "G", "3", validateDestinationSpanSizeCheck, validateExtraSpanSpaceNotFilled); // No [Flags] attribute - TryFormat(UInt16Enum.Max, "G", "Max", validateDestinationSpanSizeCheck, validateExtraSpanSpaceNotFilled); - - // "G": Int32 - TryFormat(Int32Enum.Min, "G", "Min", validateDestinationSpanSizeCheck, validateExtraSpanSpaceNotFilled); - TryFormat((Int32Enum)3, "G", "3", validateDestinationSpanSizeCheck, validateExtraSpanSpaceNotFilled); // No [Flags] attribute - TryFormat(Int32Enum.Max, "G", "Max", validateDestinationSpanSizeCheck, validateExtraSpanSpaceNotFilled); - - // "G": UInt32 - TryFormat(UInt32Enum.Min, "G", "Min", validateDestinationSpanSizeCheck, validateExtraSpanSpaceNotFilled); - TryFormat((UInt32Enum)3, "G", "3", validateDestinationSpanSizeCheck, validateExtraSpanSpaceNotFilled); // No [Flags] attribute - TryFormat(UInt32Enum.Max, "G", "Max", validateDestinationSpanSizeCheck, validateExtraSpanSpaceNotFilled); - - // "G": Int64 - TryFormat(Int64Enum.Min, "G", "Min", validateDestinationSpanSizeCheck, validateExtraSpanSpaceNotFilled); - TryFormat((Int64Enum)3, "G", "3", validateDestinationSpanSizeCheck, validateExtraSpanSpaceNotFilled); // No [Flags] attribute - TryFormat(Int64Enum.Max, "G", "Max", validateDestinationSpanSizeCheck, validateExtraSpanSpaceNotFilled); - - // "G": UInt64 - TryFormat(UInt64Enum.Min, "G", "Min", validateDestinationSpanSizeCheck, validateExtraSpanSpaceNotFilled); - TryFormat((UInt64Enum)3, "G", "3", validateDestinationSpanSizeCheck, validateExtraSpanSpaceNotFilled); // No [Flags] attribute - TryFormat(UInt64Enum.Max, "G", "Max", validateDestinationSpanSizeCheck, validateExtraSpanSpaceNotFilled); - - // "G": SimpleEnum - TryFormat((SimpleEnum)99, "G", "99", validateDestinationSpanSizeCheck, validateExtraSpanSpaceNotFilled); - TryFormat((SimpleEnum)99, "G", "99", validateDestinationSpanSizeCheck, validateExtraSpanSpaceNotFilled); - TryFormat((SimpleEnum)0, "G", "0", validateDestinationSpanSizeCheck, validateExtraSpanSpaceNotFilled); // Not found - - // "G": Flags Attribute - TryFormat(AttributeTargets.Class | AttributeTargets.Delegate, "G", "Class, Delegate", validateDestinationSpanSizeCheck, validateExtraSpanSpaceNotFilled); + foreach (string defaultFormatSpecifier in new[] { "G", null, "" }) + { + // "G": SByte + TryFormat(SByteEnum.Min, defaultFormatSpecifier, "Min", validateDestinationSpanSizeCheck, validateExtraSpanSpaceNotFilled); + TryFormat((SByteEnum)3, defaultFormatSpecifier, "3", validateDestinationSpanSizeCheck, validateExtraSpanSpaceNotFilled); // No [Flags] attribute + TryFormat(SByteEnum.Max, defaultFormatSpecifier, "Max", validateDestinationSpanSizeCheck, validateExtraSpanSpaceNotFilled); + + // "G": Byte + TryFormat(ByteEnum.Min, defaultFormatSpecifier, "Min", validateDestinationSpanSizeCheck, validateExtraSpanSpaceNotFilled); + TryFormat((ByteEnum)0xff, defaultFormatSpecifier, "Max", validateDestinationSpanSizeCheck, validateExtraSpanSpaceNotFilled); + TryFormat((ByteEnum)3, defaultFormatSpecifier, "3", validateDestinationSpanSizeCheck, validateExtraSpanSpaceNotFilled); // No [Flags] attribute + TryFormat(ByteEnum.Max, defaultFormatSpecifier, "Max", validateDestinationSpanSizeCheck, validateExtraSpanSpaceNotFilled); + + // "G": Int16 + TryFormat(Int16Enum.Min, defaultFormatSpecifier, "Min", validateDestinationSpanSizeCheck, validateExtraSpanSpaceNotFilled); + TryFormat((Int16Enum)3, defaultFormatSpecifier, "3", validateDestinationSpanSizeCheck, validateExtraSpanSpaceNotFilled); // No [Flags] attribute + TryFormat(Int16Enum.Max, defaultFormatSpecifier, "Max", validateDestinationSpanSizeCheck, validateExtraSpanSpaceNotFilled); + + // "G": UInt16 + TryFormat(UInt16Enum.Min, defaultFormatSpecifier, "Min", validateDestinationSpanSizeCheck, validateExtraSpanSpaceNotFilled); + TryFormat((UInt16Enum)3, defaultFormatSpecifier, "3", validateDestinationSpanSizeCheck, validateExtraSpanSpaceNotFilled); // No [Flags] attribute + TryFormat(UInt16Enum.Max, defaultFormatSpecifier, "Max", validateDestinationSpanSizeCheck, validateExtraSpanSpaceNotFilled); + + // "G": Int32 + TryFormat(Int32Enum.Min, defaultFormatSpecifier, "Min", validateDestinationSpanSizeCheck, validateExtraSpanSpaceNotFilled); + TryFormat((Int32Enum)3, defaultFormatSpecifier, "3", validateDestinationSpanSizeCheck, validateExtraSpanSpaceNotFilled); // No [Flags] attribute + TryFormat(Int32Enum.Max, defaultFormatSpecifier, "Max", validateDestinationSpanSizeCheck, validateExtraSpanSpaceNotFilled); + + // "G": UInt32 + TryFormat(UInt32Enum.Min, defaultFormatSpecifier, "Min", validateDestinationSpanSizeCheck, validateExtraSpanSpaceNotFilled); + TryFormat((UInt32Enum)3, defaultFormatSpecifier, "3", validateDestinationSpanSizeCheck, validateExtraSpanSpaceNotFilled); // No [Flags] attribute + TryFormat(UInt32Enum.Max, defaultFormatSpecifier, "Max", validateDestinationSpanSizeCheck, validateExtraSpanSpaceNotFilled); + + // "G": Int64 + TryFormat(Int64Enum.Min, defaultFormatSpecifier, "Min", validateDestinationSpanSizeCheck, validateExtraSpanSpaceNotFilled); + TryFormat((Int64Enum)3, defaultFormatSpecifier, "3", validateDestinationSpanSizeCheck, validateExtraSpanSpaceNotFilled); // No [Flags] attribute + TryFormat(Int64Enum.Max, defaultFormatSpecifier, "Max", validateDestinationSpanSizeCheck, validateExtraSpanSpaceNotFilled); + + // "G": UInt64 + TryFormat(UInt64Enum.Min, defaultFormatSpecifier, "Min", validateDestinationSpanSizeCheck, validateExtraSpanSpaceNotFilled); + TryFormat((UInt64Enum)3, defaultFormatSpecifier, "3", validateDestinationSpanSizeCheck, validateExtraSpanSpaceNotFilled); // No [Flags] attribute + TryFormat(UInt64Enum.Max, defaultFormatSpecifier, "Max", validateDestinationSpanSizeCheck, validateExtraSpanSpaceNotFilled); + + // "G": SimpleEnum + TryFormat((SimpleEnum)99, defaultFormatSpecifier, "99", validateDestinationSpanSizeCheck, validateExtraSpanSpaceNotFilled); + TryFormat((SimpleEnum)99, defaultFormatSpecifier, "99", validateDestinationSpanSizeCheck, validateExtraSpanSpaceNotFilled); + TryFormat((SimpleEnum)0, defaultFormatSpecifier, "0", validateDestinationSpanSizeCheck, validateExtraSpanSpaceNotFilled); // Not found + + // "G": Flags Attribute + TryFormat(AttributeTargets.Class | AttributeTargets.Delegate, defaultFormatSpecifier, "Class, Delegate", validateDestinationSpanSizeCheck, validateExtraSpanSpaceNotFilled); + } } #pragma warning disable 618 // ToString with IFormatProvider is marked as Obsolete. @@ -2313,6 +2316,11 @@ private static void TryFormat(TEnum value, ReadOnlySpan format, str { TryFormat_WithValidParameters_ReturnsTrueWithExpected(value, format, expected); } + + if (format.Length == 1 && char.IsAsciiLetterUpper(format[0])) + { + TryFormat(value, (ReadOnlySpan)new char[1] { char.ToLowerInvariant(format[0]) }, expected, validateDestinationSpanSizeCheck, validateExtraSpanSpaceNotFilled); + } } private static void TryFormat_WithValidParameters_ReturnsTrueWithExpected(TEnum value, ReadOnlySpan format, string expected) where TEnum : struct, Enum @@ -2326,7 +2334,7 @@ private static void TryFormat_WithValidParameters_ReturnsTrueWithExpected private static void TryFormat_WithDestinationSpanSizeTooSmall_ReturnsFalseWithNoCharsWritten(TEnum value, ReadOnlySpan format, string expected) where TEnum : struct, Enum { - var oneLessThanExpectedLength = expected.Length - 1; + int oneLessThanExpectedLength = expected.Length - 1; Span destination = new char[oneLessThanExpectedLength]; Assert.False(Enum.TryFormat(value, destination, out int charsWritten, format)); @@ -2336,7 +2344,7 @@ private static void TryFormat_WithDestinationSpanSizeTooSmall_ReturnsFalseWithNo private static void TryFormat_WithDestinationSpanLargerThanExpected_ReturnsTrueWithExpectedAndExtraSpaceNotFilled(TEnum value, ReadOnlySpan format, string expected) where TEnum : struct, Enum { - var oneMoreThanExpectedLength = expected.Length + 1; + int oneMoreThanExpectedLength = expected.Length + 1; Span destination = new char[oneMoreThanExpectedLength]; Assert.True(Enum.TryFormat(value, destination, out int charsWritten, format)); @@ -2355,8 +2363,6 @@ private static void TryFormat_WithEmptySpan_ReturnsFalseWithNoCharsWritten() } [Theory] - [InlineData(null)] - [InlineData("")] [InlineData(" ")] [InlineData(" ")] [InlineData(" \t")] @@ -2367,14 +2373,15 @@ private static void TryFormat_WithEmptySpan_ReturnsFalseWithNoCharsWritten() [InlineData("dd")] [InlineData("xx")] [InlineData("ff")] - private static void TryFormat_WithInvalidFormat_ReturnsFalseWithNoCharsWritten(string format) + private static void TryFormat_WithInvalidFormat_ThrowsWithNoCharsWritten(string format) { - var expecedEnum = SimpleEnum.Green; + SimpleEnum expecedEnum = SimpleEnum.Green; string expected = nameof(expecedEnum); - Span destination = new char[expected.Length]; + int charsWritten = 0; + char[] destination = new char[expected.Length]; - Assert.False(Enum.TryFormat(expecedEnum, destination, out int charsWritten, format)); - Assert.Equal(new string('\0', expected.Length), destination.ToString()); + Assert.Throws(() => Enum.TryFormat(expecedEnum, destination, out charsWritten, format)); + Assert.Equal(new string('\0', expected.Length), new string(destination)); Assert.Equal(0, charsWritten); }