首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >问答首页 >使用byte[] 10和.NET 6字符串内插将C#或ReadOnlySpan<byte>格式化为字符串,编译器处理程序降低模式

使用byte[] 10和.NET 6字符串内插将C#或ReadOnlySpan<byte>格式化为字符串,编译器处理程序降低模式
EN

Stack Overflow用户
提问于 2022-04-03 21:27:16
回答 3查看 697关注 0票数 0

我希望使用一些自定义格式参数将byte[]ReadOnlySpan<byte>字节格式化为字符串。比如说,就像S for Base64一样。为了达到这个目的,长度总是固定在某个已知的常数上。

我想使用现代的C# 10和.NET 6字符串格式化功能,如https://devblogs.microsoft.com/dotnet/string-interpolation-in-c-10-and-net-6/中描述的那样。内置类型实现ISpanFormattable,所以我想在这里引入新的格式化参数,但这样就可以使用编译器处理程序降低模式

我从那篇文章中获取了一些代码,并在嵌入的代码中做了一些修改,如下所示。它也在https://dotnetfiddle.net/svyQKD

从代码中可以看出,对于byte[],我得到了要成功的直接方法调用,而对于ReadOnlySpan<byte>,则不是这样。

有人知道怎么做吗?

我想我需要InterpolatedStringHandler。但如果是这样的话,那么我似乎不知道如何实现它。所有的提示和代码技巧可能会有帮助。我被困在这件事上已经有一段时间了,快到凌晨了。:)

代码语言:javascript
运行
复制
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));
    }
}
EN

回答 3

Stack Overflow用户

回答已采纳

发布于 2022-09-13 10:45:43

我玩这个游戏,因为我也对学习这些处理程序感兴趣,所以考虑一下我的答案。

构建错误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>,这是非法的(ReadOnlySpanref structref struct不能用作类型参数)。

正如您链接的Stephen的博客中所解释的,编译器将很高兴地扩展任何内插字符串以使用DefaultInterpolatedStringHandler (如果它愿意的话),这就是为什么您可以在Console.WriteLine中使用插值字符串,即使它没有带有DefaultInterpolatedStringHandler参数的重载:

代码语言:javascript
运行
复制
// 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>,但是在这个残酷的世界中,编译器似乎没有检测到它:

代码语言:javascript
运行
复制
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)

因此,这里有一个解决方案:使用InterpolatedStringHandler重载创建一个自定义AppendFormatted (正如您所做的那样)。与您的实现一样,我们只需要包装一个DefaultInterpolatedStringHandler (或一个StringBuilder),这样实际的实现(缓冲区管理等)就留给了更聪明的人。完整的例子:

代码语言:javascript
运行
复制
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的缓冲区中)。这就是(小得可怜的?)我们为自己没有管理缓冲区而付出的代价。如果我们不能忍受这种想法(让实现风险和收益递减见鬼),下面是一个愚蠢的处理程序,它使用一个(固定大小)缓冲区作为参数(可以进行堆栈分配):

代码语言:javascript
运行
复制
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))并继续前进:)

票数 1
EN

Stack Overflow用户

发布于 2022-04-03 21:37:11

如果您确实希望像这样使用该方法,则需要重写类ToString()Byte[]方法。

但是您不能重写类Byte[]上的方法。您需要继承类Byte[]并重写派生的ToString()方法。

然后,必须用派生类替换所有Byte[]对象,这不是一个好主意。

因此,你没有这样的解决办法:

代码语言:javascript
运行
复制
// How to make this work? Now it prints "System.Byte[]".
Console.WriteLine($"{test1:S}");

您所能做的最好是创建一个“外部”方法来格式化Byte[],并按照您的方式进行格式化。

* ReadOnlySpan<byte>也是如此。

票数 0
EN

Stack Overflow用户

发布于 2022-04-03 22:07:11

您可以使用扩展方法:

代码语言:javascript
运行
复制
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();
    }
}

结果:

代码语言:javascript
运行
复制
85 52
85 52

您也可以尝试使用:

代码语言:javascript
运行
复制
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);
}
票数 0
EN
页面原文内容由Stack Overflow提供。腾讯云小微IT领域专用引擎提供翻译支持
原文链接:

https://stackoverflow.com/questions/71729980

复制
相关文章

相似问题

领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档