C#利用插值字符串处理器写一个sscanf

 更新时间:2025年02月17日 08:21:48   作者:hez2010  
这篇文章主要为大家详细介绍了C#如何利用插值字符串处理器写一个sscanf,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下

前言

什么?用 C# 插值字符串处理器写一个输入用的 sscanf?你确定不是输出用的 sprintf

我猜不少读者看到标题后大概会有上述的想法。然而我们这里还真就是实现 sscanf,而不是 sprintf

插值字符串处理器

C# 有一个特性叫做插值字符串,使用插值字符串,你可以自然地往字符串里面插入变量的值,比如:$"abc{x}def",这一改以往通过 string.Format 来格式化字符串的方式,使得不再需要先传递一个字符串模板再挨个传递参数,非常方便。

在插值字符串的基础上更进一步,C# 支持插值字符串处理器,意味着你可以自定义字符串的插值行为。比如一个简单的例子:

[InterpolatedStringHandler]
struct Handler(int literalLength, int formattedCount)
{
    public void AppendLiteral(string s)
    {
        Console.WriteLine($"Literal: '{s}'");
    }

    public void AppendFormatted<T>(T v)
    {
        Console.WriteLine($"Value: '{v}'");
    }
}

在使用的时候,只需要把传递 string 参数的地方都换成这个 Handler 类型,就能做到按照你自定义的方式来处理插值字符串,我们的插值字符串会被 C# 编译器自动变换成 Handler 的构造和调用然后被传入:

void Foo(Handler handler) { }
var x = 42;
Foo($"abc{x}def");

比如上面这个例子,你会得到输出:

Literal: 'abc'
Value: '42'
Literal: 'def'

这大大方便了各种结构化日志框架的处理,你只需要简单的把插值字符串传递进去,日志框架就能根据你插值的方式来做到结构化解析,从而完全避免了手动去格式化字符串。

带参数的插值字符串处理器

其实 C# 的插值字符串处理器还支持带额外的参数:

[InterpolatedStringHandler]
struct Handler(int literalLength, int formattedCount, int value)
{
    public void AppendLiteral(string s)
    {
        Console.WriteLine($"Literal: '{s}'");
    }

    public void AppendFormatted<T>(T v)
    {
        Console.WriteLine($"Value: '{v}'");
    }
}

void Foo(int value, [InterpolatedStringHandlerArgument("value")] Handler handler) { }
Foo(42, $"abc{x}def");

这么一来,42 就会被传入 handler 的 value 参数当中,这允许我们捕获来自调用方的上下文,毕竟在日志场景中,根据不同参数来决定不同的格式很常见。

sscanf?

众所周知 C/C++ 里面有一个很常用的函数 sscanf,它接受一个文本输入和一个格式化模板,然后再传递对格式化部分的变量的引用,就能把变量的值解析出来:

const char* input = "test 123 test";
const char* template = "test %d test";
int v = 0;
sscanf(input, template, &v);
printf("%d\n", v); // 123

那我们能不能在 C# 里复刻一个呢?当然可以!只不过需要一点点黑魔法。

用 C# 实现 sscanf

首先我们做一个带参数的插值字符串处理器:

[InterpolatedStringHandler]
ref struct TemplatedStringHandler(int literalLength, int formattedCount, ReadOnlySpan<char> input)
{
    private ReadOnlySpan<char> _input = input;

    public void AppendLiteral(ReadOnlySpan<char> s)
    {
    }

    public void AppendFormatted<T>(T v) where T : ISpanParsable<T>
    {
    }
}

这里我们把所有的 string 都换成 ReadOnlySpan<char> 减少分配。

按照 sscanf 的使用方法,我们按理来说应该做成类似这样的东西:

void sscanf(ReadOnlySpan<char> input, ReadOnlySpan<char> template, params object[] args);

但是很显然,这里我们需要的是 (ref object)[],因为我们需要传递引用进去才能做到对外部变量的更新,而不是直接把变量的值当作 object 传进去。那怎么办呢?

你会发现,C# 的插值字符串处理器里已经包含了各变量的值,因此我们完全不需要像 C/C++ 那样通过类似 %d 之类的占位符来插入变量!相对于 "test %d test" 我们可以直接写 $"test {v} test",然后通过引用传递这个 v

一个很自然的想法是,我们把只需要把 AppendFormatted<T>(T v) 改成 AppendFormatted<T>(ref T v) 不就行了。

然而实际这么操作之后你会发现这么做是行不通的:

[InterpolatedStringHandler]
ref struct TemplatedStringHandler(int literalLength, int formattedCount, ReadOnlySpan<char> input)
{
    private ReadOnlySpan<char> _input = input;

    public void AppendLiteral(ReadOnlySpan<char> s)
    {
    }

    public void AppendFormatted<T>(ref T v) where T : ISpanParsable<T>
    {
    }
}

void sscanf(ReadOnlySpan<char> input, [InterpolatedStringHandlerArgument("input")] TemplatedStringHandler template);

当我们试图调用 sscanf 的时候:

int v = 0;
sscanf("test 123 test", $"test {ref v} test"); // error CS1525: Invalid expression term 'ref'

报错了!插值字符串的值部分里写 ref 关键字是无效的!

注意到这个错误是来自 C# 编译器的 parser,也就是说只要我们从语法上把这个 ref 干掉,那就能通过编译了。

此时我们灵机一动,我们 C# 不是有 in 来传递只读引用吗?C# 对于 in 传递只读引用会自动帮我们创建引用并传递进去,无需在语法上显式指定 ref,于是我们稍微利用一下这个特性改造一番:

[InterpolatedStringHandler]
ref struct TemplatedStringHandler(int literalLength, int formattedCount, ReadOnlySpan<char> input)
{
    private ReadOnlySpan<char> _input = input;

    public void AppendLiteral(ReadOnlySpan<char> s)
    {
    }

    public void AppendFormatted<T>(in T v) where T : ISpanParsable<T>
    {
    }
}

然后就会发现,下面这个代码可以成功编译了:

int v = 0;
sscanf("test 123 test", $"test {v} test");

此时我们离成功只剩下最后一步:传递进来的是只读引用,可是为了提取出变量我们需要更新引用的值,怎么办呢?

好在我们有 Unsafe.AsRef 把只读引用转换成可变引用,那最后一个问题解决了,我们就可以开始我们的实现了。

[InterpolatedStringHandler]
ref struct TemplatedStringHandler(int literalLength, int formattedCount, ReadOnlySpan<char> input)
{
    private int _index = 0;
    private ReadOnlySpan<char> _input = input;

    public void AppendLiteral(ReadOnlySpan<char> s)
    {
        var offset = Advance(0); // 先跳过连续空白字符
        _input = _input[offset..];
        _index += offset;
  
        if (_input.StartsWith(s)) // 从输入字符串中去掉模板字符串的非变量部分
        {
            _input = _input[s.Length..];
        }
        else throw new FormatException($"Cannot find '{s}' in the input string (at index: {_index}).");

        _index += s.Length;
        literalLength -= s.Length;
    }

    public void AppendFormatted<T>(in T v) where T : ISpanParsable<T>
    {
        var offset = Advance(0); // 先跳过连续空白字符
        _input = _input[offset..];
        _index += offset;

        var length = Scan(); // 计算到下一个空白字符为止的长度
        if (T.TryParse(_input[..length], null, out var result)) // 解析!
        {
            Unsafe.AsRef(in v) = result; // 把只读引用换成可变引用后更新引用值
            _input = _input[length..];
            _index += length;
            formattedCount--;
        }
        else
        {
            throw new FormatException($"Cannot parse '{_input[..length]}' to '{typeof(T)}' (at index: {_index}).");
        }
    }

    // 向后扫描,直到遇到空白字符停止
    private int Scan()
    {
        var length = 0;
        for (var i = 0; i < _input.Length; i++)
        {
            if (_input[i] is ' ' or '\t' or '\r' or '\n') break;
            length++;
        }
        return length;
    }

    // 跳过所有的空白字符
    private int Advance(int start)
    {
        var length = start;
        while (length < _input.Length && _input[length] is ' ' or '\t' or '\r' or '\n')
        {
            length++;
        }
        return length;
    }
}

然后我们提供一个 sscanf 暴露我们的插值字符串处理器即可:

static void sscanf(ReadOnlySpan<char> input, [InterpolatedStringHandlerArgument("input")] TemplatedStringHandler template) { }

使用

int x = 0;
string y = "";
bool z = false;
DateTime d = default;
sscanf("test 123 hello false 2025/01/01T00:00:00 end", $"test{x}{y}{z}{d}end");
Console.WriteLine(x);
Console.WriteLine(y);
Console.WriteLine(z);
Console.WriteLine(d);

得到输出:

123
hello
False
2025年1月1日 0:00:00

而 scanf 只不过是 sscanf(Console.ReadLine(), template) 的简写罢了,所以这里我们有 sscanf 就完全足够了。

结论

C# 的插值字符串处理器非常强大,利用这个特性,我们成功实现了比 C/C++ 中 sscanf 还要更好用的多的字符串解析函数,不仅不需要格式化字符串占位,还能自动推导类型,甚至连在后面的参数里逐个传递变量引用的需要都直接省掉了,在此基础上我们还做到了零分配。

到此这篇关于C#利用插值字符串处理器写一个sscanf的文章就介绍到这了,更多相关C#插值字符串内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • C#中IEnumerable、ICollection、IList、List之间的区别

    C#中IEnumerable、ICollection、IList、List之间的区别

    IEnumerable、ICollection、IList、List之间的区别,本文分别分析了它的实现源码,从而总结出了它们之间的关系和不同之处。对C# IEnumerable、ICollection、IList、List相关知识,感兴趣的朋友一起看看吧
    2021-07-07
  • Unity 制作一个分数统计系统

    Unity 制作一个分数统计系统

    项目中经常遇到分数统计的需求,例如操作正确则计分,相反则不计分失去该项分数,为了应对需求需要一个分数统计系统。本文主要介绍了通过Unity实现这样的一个计分系统,快来跟随小编一起学习吧
    2021-12-12
  • C# Windows API应用之基于FlashWindowEx实现窗口闪烁的方法

    C# Windows API应用之基于FlashWindowEx实现窗口闪烁的方法

    这篇文章主要介绍了C# Windows API应用之基于FlashWindowEx实现窗口闪烁的方法,结合实例形式分析了Windows API函数FlashWindowEx的功能、定义及实现窗口闪烁的相关技巧,需要的朋友可以参考下
    2016-08-08
  • C#使用ffmpeg实现将图片保存为mp4视频

    C#使用ffmpeg实现将图片保存为mp4视频

    FFmpeg是一个开源的跨平台多媒体处理工具,它提供了强大的功能,包括频和视频编码、解码、转码等,本文我们将使用FFmpeg实现将图片保存为mp4视频,感兴趣的可以了解下
    2024-11-11
  • C#中虚方法virtual示例详解

    C#中虚方法virtual示例详解

    这篇文章主要介绍了C#中虚方法virtual的相关知识,本文给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友参考下吧
    2025-03-03
  • 一些C#常见面试题目以及答案总结

    一些C#常见面试题目以及答案总结

    这篇文章主要介绍了C#常见面试题目以及答案的相关资料,分别是C#编程语言中的委托、多线程、事件、异常处理、异步编程、LINQ、内存管理、图像处理、内存管理、设计模式等概念和实现方法,需要的朋友可以参考下
    2025-03-03
  • C#中实现比较两个对象是否相等的7个方法总结

    C#中实现比较两个对象是否相等的7个方法总结

    在 C# 中,要比较两个对象实例是否相等,有一些常用的方法,比如实现 IEquatable<T> 接口、重写 Object.Equals 方法,或使用自定义比较逻辑等等,本文为大家总结了7 种常用的方法,大家可以根据需要进行选择
    2025-11-11
  • C#中队列排序的实践方法

    C#中队列排序的实践方法

    本文主要介绍了C#中队列排序的实践方法,通过创建自定义优先级队列实现排序,文中通过示例代码介绍的非常详细,需要的朋友们下面随着小编来一起学习学习吧
    2026-05-05
  • 解析C#编程的通用结构和程序书写格式规范

    解析C#编程的通用结构和程序书写格式规范

    这篇文章主要介绍了C#编程的通用结构和程序书写格式规范,这里我们根据C#语言的开发方微软给出的约定来作为编写样式参照,需要的朋友可以参考下
    2016-01-01
  • 客户端实现蓝牙接收(C#)知识总结

    客户端实现蓝牙接收(C#)知识总结

    网上有关蓝牙接收的资料很多,使用起来也很简单,但是我觉得还是有必要把这些知识总结下来,蓝牙开发需要用到一个第三方的库InTheHand.Net.Personal.dll,感兴趣的朋友可以了解下,或许对你有所帮助
    2013-02-02

最新评论