欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

C#利用插值字符串處理器寫一個sscanf

 更新時間:2025年02月17日 08:21:48   作者:hez2010  
這篇文章主要為大家詳細介紹了C#如何利用插值字符串處理器寫一個sscanf,文中的示例代碼講解詳細,感興趣的小伙伴可以跟隨小編一起學(xué)習(xí)一下

前言

什么?用 C# 插值字符串處理器寫一個輸入用的 sscanf?你確定不是輸出用的 sprintf

我猜不少讀者看到標(biāo)題后大概會有上述的想法。然而我們這里還真就是實現(xiàn) sscanf,而不是 sprintf。

插值字符串處理器

C# 有一個特性叫做插值字符串,使用插值字符串,你可以自然地往字符串里面插入變量的值,比如:$"abc{x}def",這一改以往通過 string.Format 來格式化字符串的方式,使得不再需要先傳遞一個字符串模板再挨個傳遞參數(shù),非常方便。

在插值字符串的基礎(chǔ)上更進一步,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 參數(shù)的地方都換成這個 Handler 類型,就能做到按照你自定義的方式來處理插值字符串,我們的插值字符串會被 C# 編譯器自動變換成 Handler 的構(gòu)造和調(diào)用然后被傳入:

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

比如上面這個例子,你會得到輸出:

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

這大大方便了各種結(jié)構(gòu)化日志框架的處理,你只需要簡單的把插值字符串傳遞進去,日志框架就能根據(jù)你插值的方式來做到結(jié)構(gòu)化解析,從而完全避免了手動去格式化字符串。

帶參數(shù)的插值字符串處理器

其實 C# 的插值字符串處理器還支持帶額外的參數(shù):

[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 參數(shù)當(dāng)中,這允許我們捕獲來自調(diào)用方的上下文,畢竟在日志場景中,根據(jù)不同參數(shù)來決定不同的格式很常見。

sscanf?

眾所周知 C/C++ 里面有一個很常用的函數(shù) 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# 里復(fù)刻一個呢?當(dāng)然可以!只不過需要一點點黑魔法。

用 C# 實現(xiàn) sscanf

首先我們做一個帶參數(shù)的插值字符串處理器:

[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 的使用方法,我們按理來說應(yīng)該做成類似這樣的東西:

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

但是很顯然,這里我們需要的是 (ref object)[],因為我們需要傳遞引用進去才能做到對外部變量的更新,而不是直接把變量的值當(dāng)作 object 傳進去。那怎么辦呢?

你會發(fā)現(xiàn),C# 的插值字符串處理器里已經(jīng)包含了各變量的值,因此我們完全不需要像 C/C++ 那樣通過類似 %d 之類的占位符來插入變量!相對于 "test %d test" 我們可以直接寫 $"test {v} test",然后通過引用傳遞這個 v。

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

然而實際這么操作之后你會發(fā)現(xiàn)這么做是行不通的:

[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);

當(dāng)我們試圖調(diào)用 sscanf 的時候:

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

報錯了!插值字符串的值部分里寫 ref 關(guān)鍵字是無效的!

注意到這個錯誤是來自 C# 編譯器的 parser,也就是說只要我們從語法上把這個 ref 干掉,那就能通過編譯了。

此時我們靈機一動,我們 C# 不是有 in 來傳遞只讀引用嗎?C# 對于 in 傳遞只讀引用會自動幫我們創(chuàng)建引用并傳遞進去,無需在語法上顯式指定 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>
    {
    }
}

然后就會發(fā)現(xiàn),下面這個代碼可以成功編譯了:

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

此時我們離成功只剩下最后一步:傳遞進來的是只讀引用,可是為了提取出變量我們需要更新引用的值,怎么辦呢?

好在我們有 Unsafe.AsRef 把只讀引用轉(zhuǎn)換成可變引用,那最后一個問題解決了,我們就可以開始我們的實現(xiàn)了。

[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); // 先跳過連續(xù)空白字符
        _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); // 先跳過連續(xù)空白字符
        _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}vvxyksv9kdend");
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 就完全足夠了。

結(jié)論

C# 的插值字符串處理器非常強大,利用這個特性,我們成功實現(xiàn)了比 C/C++ 中 sscanf 還要更好用的多的字符串解析函數(shù),不僅不需要格式化字符串占位,還能自動推導(dǎo)類型,甚至連在后面的參數(shù)里逐個傳遞變量引用的需要都直接省掉了,在此基礎(chǔ)上我們還做到了零分配。

到此這篇關(guān)于C#利用插值字符串處理器寫一個sscanf的文章就介紹到這了,更多相關(guān)C#插值字符串內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!

您可能感興趣的文章:

相關(guān)文章

  • C#實現(xiàn)優(yōu)先隊列和堆排序

    C#實現(xiàn)優(yōu)先隊列和堆排序

    本文詳細講解了C#實現(xiàn)優(yōu)先隊列和堆排序的方法,文中通過示例代碼介紹的非常詳細。對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下
    2022-04-04
  • WCF入門教程之Windows通訊接口

    WCF入門教程之Windows通訊接口

    這篇文章介紹了WCF入門教程之Windows通訊接口,文中通過示例代碼介紹的非常詳細。對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下
    2022-05-05
  • 在C#中生成PDF的步驟詳解

    在C#中生成PDF的步驟詳解

    在?.NET?世界中,存在許多?PDF?庫,但我們發(fā)現(xiàn),使用集成瀏覽器生成?PDF?更為簡單,由于?DotNetBrowser?可以在完全脫離屏幕的情況下工作,本文給大家介紹了如何在?C#?中生成?PDF,需要的朋友可以參考下
    2024-10-10
  • C#繪制鼠標(biāo)指針的示例代碼

    C#繪制鼠標(biāo)指針的示例代碼

    這篇文章主要為大家詳細介紹了如何利用C#實現(xiàn)將鼠標(biāo)的指針樣式給繪制成圖片,顯示或者保存下來,文中的示例代碼講解詳細,需要的可以參考一下
    2024-01-01
  • C#調(diào)用sql2000存儲過程方法小結(jié)

    C#調(diào)用sql2000存儲過程方法小結(jié)

    這篇文章主要介紹了C#調(diào)用sql2000存儲過程的方法,以實例形式分別對調(diào)用帶輸入?yún)?shù)及輸出參數(shù)的存儲過程進行了詳細分析,非常具有實用價值,需要的朋友可以參考下
    2014-10-10
  • 如何最大限度地降低多線程C#代碼的復(fù)雜性

    如何最大限度地降低多線程C#代碼的復(fù)雜性

    這篇文章主要介紹了如何最大限度地降低多線程C#代碼的復(fù)雜性,小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧
    2019-03-03
  • 最新評論