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 傳進去。那怎么辦呢?
你會發(fā)現,C# 的插值字符串處理器里已經包含了各變量的值,因此我們完全不需要像 C/C++ 那樣通過類似 %d 之類的占位符來插入變量!相對于 "test %d test" 我們可以直接寫 $"test {v} test",然后通過引用傳遞這個 v。
一個很自然的想法是,我們把只需要把 AppendFormatted<T>(T v) 改成 AppendFormatted<T>(ref T v) 不就行了。
然而實際這么操作之后你會發(fā)現這么做是行不通的:
[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 傳遞只讀引用會自動幫我們創(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ā)現,下面這個代碼可以成功編譯了:
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); // 先跳過連續(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}1nhlj9hend");
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#插值字符串內容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
Unity的AssetPostprocessor之Model函數使用實戰(zhàn)
這篇文章主要為大家介紹了Unity的AssetPostprocessor之Model函數使用實戰(zhàn),有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2023-08-08
Winform開發(fā)框架中如何使用DevExpress的內置圖標資源
這篇文章主要給大家介紹了關于在Winform開發(fā)框架中如何使用DevExpress的內置圖標資源的相關資料,文中通過圖文介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們一起來看看吧2018-12-12

