[英]Formatting byte[] or ReadOnlySpan<byte> to string using C# 10 and .NET 6 string interpolation, compiler handler lowering pattern
我想使用一些自定义格式参数将byte[]
和ReadOnlySpan<byte>
字节格式化为字符串。 比如说,就像Base64
的S
一样。 为此,长度始终固定为某个已知常数。
我想使用现代 C# 10 和 .NET 6 字符串格式化功能,如https://devblogs.microsoft.com/dotnet/inand-net-6-6-c-10 内置类型实现ISpanFormattable ,所以我想在这里带来新的格式化参数,但是使用编译器处理程序降低模式。
我从该帖子中获取了一些代码,并在嵌入的代码中对其进行了一些修改,如下所示。 它也在https://dotnetfiddle.net/svyQKD 。
从代码中可以看出,我得到了byte[]
的直接方法调用成功,但ReadOnlySpan<byte>
没有成功。
有谁知道如何做到这一点?
我怀疑我需要InterpolatedStringHandler 。 但如果是这样的话,那么看起来我不知道如何实现一个。 所有提示和代码技巧都可能会有所帮助。 我已经被困了一段时间了,现在已经到凌晨了。 :)
using System;
using System.Globalization;
using System.Runtime.CompilerServices;
public class Program
{
public sealed class ExampleCustomFormatter: IFormatProvider, ICustomFormatter
{
public object? GetFormat(Type? formatType) => formatType == typeof(ICustomFormatter) ? this : null;
public string Format(string? format, object? arg, IFormatProvider? formatProvider) => format == "S" && arg is byte[] i ? Convert.ToBase64String(i) : arg is IFormattable formattable ? formattable.ToString(format, formatProvider) : arg?.ToString() ?? string.Empty;
}
public static class StringExtensions
{
public static string FormatString(byte[] buffer) => string.Create(new ExampleCustomFormatter(), stackalloc char[64], $"{buffer:S}");
// How to make this work? Maybe needs to have TryWrite
// public static string FormatString2(ReadOnlySpan<byte> buffer) => string.Create(new ExampleCustomFormatter(), stackalloc char[64], $"{buffer:S}");
}
[InterpolatedStringHandler]
public ref struct BinaryMessageInterpolatedStringHandler
{
private readonly DefaultInterpolatedStringHandler handler;
public BinaryMessageInterpolatedStringHandler(int literalLength, int formattedCount, bool predicate, out bool handlerIsValid)
{
handler = default;
if(predicate)
{
handlerIsValid = false;
return;
}
handlerIsValid = true;
handler = new DefaultInterpolatedStringHandler(literalLength, formattedCount);
}
public void AppendLiteral(string s) => handler.AppendLiteral(s);
public void AppendFormatted<T>(T t) => handler.AppendFormatted(t);
public override string ToString() => handler.ToStringAndClear();
}
public static void Main()
{
byte[] test1 = new byte[1] { 0x55 };
ReadOnlySpan<byte> test2 = new byte[1] { 0x55 };
// How to make this work? Now it prints "System.Byte[]".
Console.WriteLine($"{test1:S}");
// This works.
Console.WriteLine(StringExtensions.FormatString(test1));
// How to make this work? This does not compile. (Yes, signature problem. How to define it?).
// Console.WriteLine($"{test2:S}");
// How to make this work? This does not compile. (Yes, signature problem. How to define it?).
// Console.WriteLine(StringExtensions.FormatString(test2));
}
}
如果你真的想使用这样的方法,你需要重写 class Byte[]
的ToString()
方法。
但是您不能在非常 class Byte[]
上覆盖该方法。 您需要继承 class Byte[]
并覆盖派生的ToString()
方法。
然后,您必须用派生的 class 替换所有Byte[]
对象,这不是一个好主意。
因此,这种方式没有适合您的解决方案:
// How to make this work? Now it prints "System.Byte[]".
Console.WriteLine($"{test1:S}");
您可以做的最好的事情是创建一个“外部”方法来格式化Byte[]
并按照您的方式进行格式化。
*这同样适用于ReadOnlySpan<byte>
。
您可以使用扩展方法:
using System.Text;
byte[] test1 = new byte[2] { 0x55, 0x34 };
ReadOnlySpan<byte> test2 = new byte[2] { 0x55, 0x34 };
// How to make this work? Now it prints "System.Byte[]".
Console.WriteLine($"{test1.MyFormat()}");
Console.WriteLine($"{test2.MyFormat()}");
public static class MyExtensionMethods
{
public static string MyFormat(this byte[] value)
{
StringBuilder sb = new StringBuilder();
foreach (byte b in value)
{
sb.Append(b).Append(" ");
}
return sb.ToString();
}
public static string MyFormat(this ReadOnlySpan<byte> value)
{
StringBuilder sb = new StringBuilder();
foreach (byte b in value)
{
sb.Append(b).Append(" ");
}
return sb.ToString();
}
}
结果:
85 52
85 52
您也可以尝试:
public static class MyExtensionMethods
{
public static string MyFormat(this byte[] value) => Encoding.Unicode.GetString(value);
public static string MyFormat(this ReadOnlySpan<byte> value) => Encoding.Unicode.GetString(value);
}
我玩弄了这个,因为我也有兴趣了解这些处理程序,所以请考虑一下我的回答。
构建错误CS0306 The type 'ReadOnlySpan<byte>' may not be used as a type argument
on Console.WriteLine($"{test2:S}");
(和StringExtensions.FormatString2
)源于(我相信)编译器生成的对DefaultInterpolatedStringHandler.AppendFormatted<T>
的调用,泛型类型参数ReadOnlySpan<byte>
这是非法的( ReadOnlySpan
是一个ref struct
,一个ref struct
不能用作类型参数)。
正如您链接的 Stephen Toub 的博客中所解释的那样,编译器会很高兴地扩展任何插值字符串以使用DefaultInterpolatedStringHandler
(如果需要),这就是为什么即使它没有重载也可以将插值字符串与Console.WriteLine
一起使用DefaultInterpolatedStringHandler
参数:
// This trivial example might not actually construct a DefaultInterpolatedStringHandler
// but the point still stands (it compiles and works)
int x = 3;
Console.WriteLine($"{x}");
一个简单的解决方案是创建DefaultInterpolatedStringHandler.AppendFormatted
的扩展重载,采用ReadOnlySpan<byte>
但在这个残酷的世界中,编译器似乎没有检测到它:
public static class PleaseWork
{
public static void AppendFormatted(
this ref DefaultInterpolatedStringHandler handler,
ReadOnlySpan<byte> bytes,
string? format)
{
if (format != "S")
{
throw new ArgumentException("Invalid format");
}
handler.AppendLiteral(Convert.ToBase64String(bytes));
}
}
ReadOnlySpan<byte> test2 = new byte[1] { 0x56 };
Console.WriteLine($"{test2:S}"); // nope (CS0306)
所以这是一个解决方案:使用AppendFormatted
重载创建一个自定义InterpolatedStringHandler
(就像你所做的那样)。 就像在您的实现中一样,我们可以只包装一个DefaultInterpolatedStringHandler
(或StringBuilder
),以便将实际实现(缓冲区管理等)留给更聪明的人。 完整示例:
public class Program
{
[InterpolatedStringHandler]
public ref struct WrappingInterpolatedStringHandler
{
private DefaultInterpolatedStringHandler handler;
public WrappingInterpolatedStringHandler(int literalLength, int formattedCount)
{
handler = new(literalLength, formattedCount);
}
public void AppendLiteral(string s) => handler.AppendLiteral(s);
public void AppendFormatted<T>(T t, string? format = null)
=> handler.AppendFormatted(t, format);
// This is necessary otherwise byte[] arg resolves to the generic overload
public void AppendFormatted(byte[] bytes, string? format = null)
=> AppendFormatted((ReadOnlySpan<byte>)bytes, format);
public void AppendFormatted(ReadOnlySpan<byte> bytes, string? format)
{
if (format != "S")
{
throw new ArgumentException("Invalid format");
}
// This allocates an intermediate string
handler.AppendLiteral(Convert.ToBase64String(bytes));
}
public string ToStringAndClear() => handler.ToStringAndClear();
}
public static void Main()
{
byte[] test1 = new byte[1] { 0x55 };
ReadOnlySpan<byte> test2 = new byte[1] { 0x56 };
Span<byte> test3 = new byte[128];
test3.Fill(0x55);
WriteLine($"test1:{test1:S}"); // "test1:VQ=="
WriteLine($"test2:{test2:S}"); // "test2:Vg=="
WriteLine($"test3:{test3:S}"); // "test3:<a long base64 string>"
}
private static void WriteLine(ref WrappingInterpolatedStringHandler builder)
{
// Naturally, Console.WriteLine has no overload taking our custom handler
// so we need to give it the resulting string
Console.WriteLine(builder.ToStringAndClear());
}
}
我们新的AppendFormatted
重载调用Convert.ToBase64String
分配一个中间string
(然后将其复制到DefaultInterpolatedStringHandler
的缓冲区中)。 这就是我们为自己不管理缓冲区所付出的(小得惊人的?)代价。 如果我们无法忍受这个想法(实施风险和收益递减的地狱),这里有一个愚蠢的处理程序,将(固定大小)缓冲区作为参数(可以堆栈分配):
public class Program
{
[InterpolatedStringHandler]
public ref struct MyReallyBadAndUntestedInterpolatedStringHandler
{
private readonly Span<char> _buffer;
private int _charsWritten;
public MyReallyBadAndUntestedInterpolatedStringHandler(int literalLength, int formattedCount, Span<char> destination)
{
_buffer = destination;
_charsWritten = 0;
}
public bool AppendLiteral(string s)
{
if (s.AsSpan().TryCopyTo(_buffer.Slice(_charsWritten)))
{
_charsWritten += s.Length;
return true;
}
return false;
}
public bool AppendFormatted<T>(T t, ReadOnlySpan<char> format)
{
Span<char> buffer = _buffer.Slice(_charsWritten);
if (t is ISpanFormattable formattable)
{
bool success = formattable.TryFormat(buffer, out int charsWritten, format, null);
_charsWritten += _charsWritten;
return success;
}
string s = t?.ToString() ?? "";
if (s.AsSpan().TryCopyTo(buffer))
{
_charsWritten += s.Length;
return true;
}
return false;
}
// This is necessary otherwise byte[] arg resolves to the generic overload
public bool AppendFormatted(byte[] bytes, ReadOnlySpan<char> format)
=> AppendFormatted((ReadOnlySpan<byte>)bytes, format);
public bool AppendFormatted(ReadOnlySpan<byte> bytes, ReadOnlySpan<char> format)
{
if (format != "S")
{
throw new ArgumentException("Invalid format");
}
bool success = Convert.TryToBase64Chars(bytes, _buffer.Slice(_charsWritten), out int charsWritten);
_charsWritten += charsWritten;
return success;
}
public string ToStringAndClear()
{
string result = new string(_buffer.Slice(0, _charsWritten));
_buffer.Clear();
_charsWritten = 0;
return result;
}
}
public static void Main()
{
byte[] test1 = new byte[1] { 0x55 };
ReadOnlySpan<byte> test2 = new byte[1] { 0x56 };
Span<byte> test3 = new byte[128];
test3.Fill(0x55);
Span<char> buffer = stackalloc char[64];
WriteLine(buffer, $"test1:{test1:S}"); // "test1:VQ=="
WriteLine(buffer, $"test2:{test2:S}"); // "test2:Vg=="
WriteLine(buffer, $"test3:{test3:S}"); // "test3:" (the buffer is not big enough)
}
private static void WriteLine(
Span<char> destination,
[InterpolatedStringHandlerArgument("destination")] ref MyReallyBadAndUntestedInterpolatedStringHandler builder)
{
Console.WriteLine(builder.ToStringAndClear());
}
}
总之,只需调用Console.WriteLine(Convert.ToBase64String(test2))
并继续:)
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.