簡體   English   中英

格式化 byte[] 或 ReadOnlySpan<byte> 使用 C# 10 和 .NET 6 字符串插值,編譯器處理程序降低模式</byte>

[英]Formatting byte[] or ReadOnlySpan<byte> to string using C# 10 and .NET 6 string interpolation, compiler handler lowering pattern

我想使用一些自定義格式參數將byte[]ReadOnlySpan<byte>字節格式化為字符串。 比如說,就像Base64S一樣。 為此,長度始終固定為某個已知常數。

我想使用現代 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.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM