C#利用插值字符串處理器寫一個(gè)sscanf
前言
什么?用 C# 插值字符串處理器寫一個(gè)輸入用的 sscanf?你確定不是輸出用的 sprintf?
我猜不少讀者看到標(biāo)題后大概會(huì)有上述的想法。然而我們這里還真就是實(shí)現(xiàn) sscanf,而不是 sprintf。
插值字符串處理器
C# 有一個(gè)特性叫做插值字符串,使用插值字符串,你可以自然地往字符串里面插入變量的值,比如:$"abc{x}def",這一改以往通過 string.Format 來格式化字符串的方式,使得不再需要先傳遞一個(gè)字符串模板再挨個(gè)傳遞參數(shù),非常方便。
在插值字符串的基礎(chǔ)上更進(jìn)一步,C# 支持插值字符串處理器,意味著你可以自定義字符串的插值行為。比如一個(gè)簡(jiǎn)單的例子:
[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}'");
}
}在使用的時(shí)候,只需要把傳遞 string 參數(shù)的地方都換成這個(gè) Handler 類型,就能做到按照你自定義的方式來處理插值字符串,我們的插值字符串會(huì)被 C# 編譯器自動(dòng)變換成 Handler 的構(gòu)造和調(diào)用然后被傳入:
void Foo(Handler handler) { }
var x = 42;
Foo($"abc{x}def");比如上面這個(gè)例子,你會(huì)得到輸出:
Literal: 'abc'
Value: '42'
Literal: 'def'
這大大方便了各種結(jié)構(gòu)化日志框架的處理,你只需要簡(jiǎn)單的把插值字符串傳遞進(jìn)去,日志框架就能根據(jù)你插值的方式來做到結(jié)構(gòu)化解析,從而完全避免了手動(dòng)去格式化字符串。
帶參數(shù)的插值字符串處理器
其實(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 就會(huì)被傳入 handler 的 value 參數(shù)當(dāng)中,這允許我們捕獲來自調(diào)用方的上下文,畢竟在日志場(chǎng)景中,根據(jù)不同參數(shù)來決定不同的格式很常見。
sscanf?
眾所周知 C/C++ 里面有一個(gè)很常用的函數(shù) sscanf,它接受一個(gè)文本輸入和一個(gè)格式化模板,然后再傳遞對(duì)格式化部分的變量的引用,就能把變量的值解析出來:
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ù)刻一個(gè)呢?當(dāng)然可以!只不過需要一點(diǎn)點(diǎn)黑魔法。
用 C# 實(shí)現(xiàn) sscanf
首先我們做一個(gè)帶參數(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)[],因?yàn)槲覀冃枰獋鬟f引用進(jìn)去才能做到對(duì)外部變量的更新,而不是直接把變量的值當(dāng)作 object 傳進(jìn)去。那怎么辦呢?
你會(huì)發(fā)現(xiàn),C# 的插值字符串處理器里已經(jīng)包含了各變量的值,因此我們完全不需要像 C/C++ 那樣通過類似 %d 之類的占位符來插入變量!相對(duì)于 "test %d test" 我們可以直接寫 $"test {v} test",然后通過引用傳遞這個(gè) v。
一個(gè)很自然的想法是,我們把只需要把 AppendFormatted<T>(T v) 改成 AppendFormatted<T>(ref T v) 不就行了。
然而實(shí)際這么操作之后你會(huì)發(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)我們?cè)噲D調(diào)用 sscanf 的時(shí)候:
int v = 0;
sscanf("test 123 test", $"test {ref v} test"); // error CS1525: Invalid expression term 'ref'報(bào)錯(cuò)了!插值字符串的值部分里寫 ref 關(guān)鍵字是無效的!
注意到這個(gè)錯(cuò)誤是來自 C# 編譯器的 parser,也就是說只要我們從語法上把這個(gè) ref 干掉,那就能通過編譯了。
此時(shí)我們靈機(jī)一動(dòng),我們 C# 不是有 in 來傳遞只讀引用嗎?C# 對(duì)于 in 傳遞只讀引用會(huì)自動(dòng)幫我們創(chuàng)建引用并傳遞進(jìn)去,無需在語法上顯式指定 ref,于是我們稍微利用一下這個(gè)特性改造一番:
[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>
{
}
}然后就會(huì)發(fā)現(xiàn),下面這個(gè)代碼可以成功編譯了:
int v = 0;
sscanf("test 123 test", $"test {v} test");此時(shí)我們離成功只剩下最后一步:傳遞進(jìn)來的是只讀引用,可是為了提取出變量我們需要更新引用的值,怎么辦呢?
好在我們有 Unsafe.AsRef 把只讀引用轉(zhuǎn)換成可變引用,那最后一個(gè)問題解決了,我們就可以開始我們的實(shí)現(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(); // 計(jì)算到下一個(gè)空白字符為止的長(zhǎng)度
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;
}
}然后我們提供一個(gè) 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) 的簡(jiǎn)寫罷了,所以這里我們有 sscanf 就完全足夠了。
結(jié)論
C# 的插值字符串處理器非常強(qiáng)大,利用這個(gè)特性,我們成功實(shí)現(xiàn)了比 C/C++ 中 sscanf 還要更好用的多的字符串解析函數(shù),不僅不需要格式化字符串占位,還能自動(dòng)推導(dǎo)類型,甚至連在后面的參數(shù)里逐個(gè)傳遞變量引用的需要都直接省掉了,在此基礎(chǔ)上我們還做到了零分配。
到此這篇關(guān)于C#利用插值字符串處理器寫一個(gè)sscanf的文章就介紹到這了,更多相關(guān)C#插值字符串內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Unity的AssetPostprocessor之Model函數(shù)使用實(shí)戰(zhàn)
這篇文章主要為大家介紹了Unity的AssetPostprocessor之Model函數(shù)使用實(shí)戰(zhàn),有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-08-08
C#實(shí)現(xiàn)HTTP訪問類HttpHelper的示例詳解
在項(xiàng)目開發(fā)過程中,我們經(jīng)常會(huì)訪問第三方接口,如我們需要接入的第三方接口是Web API,這時(shí)候我們就需要使用HttpHelper調(diào)用遠(yuǎn)程接口了。本文為大家介紹了C#實(shí)現(xiàn)HTTP訪問類HttpHelper的示例代碼,需要的可以參考一下2022-09-09
C#正則匹配RegexOptions選項(xiàng)的組合使用方法
本文主要簡(jiǎn)單介紹RegexOptions各種選項(xiàng)的作用,并介紹如何組合使用,為初學(xué)者解除一些疑惑。2016-04-04
簡(jiǎn)介Winform中創(chuàng)建用戶控件
用戶控件可以讓開發(fā)人員對(duì)VS控件進(jìn)行組裝。下面我們來創(chuàng)建一個(gè)按鈕的用戶控件我們可以給它添加屬性,并且添加相應(yīng)鼠標(biāo)移入、移出事件。2013-03-03
Winform開發(fā)框架中如何使用DevExpress的內(nèi)置圖標(biāo)資源
這篇文章主要給大家介紹了關(guān)于在Winform開發(fā)框架中如何使用DevExpress的內(nèi)置圖標(biāo)資源的相關(guān)資料,文中通過圖文介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們一起來看看吧2018-12-12
c#利用Excel直接讀取數(shù)據(jù)到DataGridView
這個(gè)例子的功能是c#讀取excel文件,大家可以參考使用2013-11-11

