如何在const字符串中嵌入一个const char?
请考虑以下两行代码:
const char c = 'a';
const string s = "" + c;
第二行会给我 error CS0133: The expression being assigned to 's' must be constant。
请注意,如果我尝试以下代码,也会得到同样的错误:
const string s = $"{c}";
虽然为了便于讨论,这里可以忽略这个。
我的问题:
问题#1:
我在这里实际上想要实现的,是在一个 const string 中嵌入一个 const char,该怎么做?
问题#2:(加分题)
我在这里违反了标准的哪一部分?据我所知,相关的部分是7.19 “常量表达式”,其中说:
常量表达式必须是空字面量或具有下列类型之一的值: [...] char, [...] string [...].只有以下构造可在常量表达式中使用:
- 字面量(包括空字面量)。
[...]
- 对const参数或局部变量的引用
- 预定义的+、–、*、[...] 二元运算符,前提是每个操作数的类型如上所列。
[...]
解决方案
按相反顺序回答:
- 问题#3:(来自评论)
我对字符串有一个switch表达式,因此它们必须是const,并且我对字符也有一个switch表达式,因此它们也必须是const。所以现在我必须重复这些常量。我希望不要再重复它们。
理想情况下,应该完全消除一组常量的值,要么是 const string,要么是 const char。但到底应该删掉哪一组呢?
如果你所有的 const string 测试值的长度都是一位,你可以完全将它们删除,并把你的字符串switch表达式改为匹配单个字符。这可以通过创建如下扩展方法来返回长度为一的字符串的单个字符来轻松实现:
public static partial class Extensions
{
public static char? SingleOrDefault(this string? s) => s?.Length == 1 ? s[0] : null;
}
然后例如如下使用:
const char achar = 'a';
const char bchar = 'b';
const char cchar = 'c';
public static string CharMatch(string s) =>
s.SingleOrDefault() switch
{
achar => "matched " + achar,
bchar => "matched " + bchar,
cchar => "matched " + cchar,
_ => "not found", // String length != 1 or character not found
};
但是如果你的一些 const string 的switch表达式值长度大于一个字符,那么就不能将它们删除。相反,你可以删除你的 const char 值,并把字符的switch表达式转换为字符串的switch。这将需要你把你的 char 值转换成一个可以用于对字符串进行模式匹配的形式。当然你也可以使用 char.ToString() 来实现,但如果你担心为每次switch分配一个字符串的性能开销,你也可以将字符转换为一个stackalloc的 ReadOnlySpan<char>,并对其进行 switch 的比较。这样就完全消除了内存分配。首先创建以下扩展方法:
public static partial class Extensions
{
public static ReadOnlySpan<T> AsReadOnlySpan<T>(this ref T item) where T : unmanaged
=> new (ref item);
}
然后例如如下使用:
const string astring = "a";
const string bstring = "b";
const string cstring = "c";
public static string SpanMatch(char c) =>
c.AsReadOnlySpan() switch
{
astring => "matched " + astring,
bstring => "matched " + bstring,
cstring => "matched " + cstring,
_ => "not found",
};
现在让我们来评估这个选项的性能。我创建了三个不同的模式匹配方法,参数为 char c:
- 一个
charswitch。 - 一个
ReadOnlySpan<char>switch。 - 一个
char.ToString()switch。
并在.NET 10上使用 BenchmarkDotNet 进行基准测试:
public class Matches
{
const string astring = "a";
const string bstring = "b";
const string cstring = "c";
const string dstring = "d";
const string estring = "e";
const string fstring = "f";
const char achar = 'a';
const char bchar = 'b';
const char cchar = 'c';
const char dchar = 'd';
const char echar = 'e';
const char fchar = 'f';
public static string CharMatch(char c) =>
c switch
{
achar => "matched " + astring,
bchar => "matched " + bstring,
cchar => "matched " + cstring,
dchar => "matched " + dstring,
echar => "matched " + estring,
fchar => "matched " + fstring,
_ => "not found",
};
public static string SpanMatch(char c) =>
c.AsReadOnlySpan() switch
{
astring => "matched " + astring,
bstring => "matched " + bstring,
cstring => "matched " + cstring,
dstring => "matched " + dstring,
estring => "matched " + estring,
fstring => "matched " + fstring,
_ => "not found",
};
public static string StringMatch(char c) =>
c.ToString() switch
{
astring => "matched " + astring,
bstring => "matched " + bstring,
cstring => "matched " + cstring,
dstring => "matched " + dstring,
estring => "matched " + estring,
fstring => "matched " + fstring,
_ => "not found",
};
}
public class TestClass
{
[Benchmark]
[ArgumentsSource(nameof(TestChars))]
public string TestCharMatch(char ch) => Matches.CharMatch(ch);
[Benchmark]
[ArgumentsSource(nameof(TestChars))]
public string TestSpanMatch(char ch) => Matches.SpanMatch(ch);
[Benchmark]
[ArgumentsSource(nameof(TestChars))]
public string TestStringMatch(char ch) => Matches.StringMatch(ch);
public char [] TestChars() => ['a', 'b', 'c', 'd', 'e', 'f', 'z'];
}
下面是结果:
| Method | ch | Mean | Error | StdDev |
|---------------- |--- |----------:|----------:|----------:|
| TestCharMatch | a | 0.9597 ns | 0.0453 ns | 0.0424 ns |
| TestSpanMatch | a | 0.7359 ns | 0.0143 ns | 0.0119 ns |
| TestStringMatch | a | 3.6467 ns | 0.0223 ns | 0.0198 ns |
| TestCharMatch | b | 0.9950 ns | 0.0308 ns | 0.0288 ns |
| TestSpanMatch | b | 1.2698 ns | 0.0226 ns | 0.0201 ns |
| TestStringMatch | b | 4.3160 ns | 0.0318 ns | 0.0266 ns |
| TestCharMatch | c | 0.9660 ns | 0.0245 ns | 0.0230 ns |
| TestSpanMatch | c | 1.6254 ns | 0.0243 ns | 0.0227 ns |
| TestStringMatch | c | 4.6965 ns | 0.0205 ns | 0.0182 ns |
| TestCharMatch | d | 0.9702 ns | 0.0201 ns | 0.0188 ns |
| TestSpanMatch | d | 1.9552 ns | 0.0342 ns | 0.0320 ns |
| TestStringMatch | d | 4.7783 ns | 0.0401 ns | 0.0335 ns |
| TestCharMatch | e | 0.9698 ns | 0.0288 ns | 0.0270 ns |
| TestSpanMatch | e | 2.5572 ns | 0.0253 ns | 0.0237 ns |
| TestStringMatch | e | 5.2879 ns | 0.1572 ns | 0.1544 ns |
| TestCharMatch | f | 0.9608 ns | 0.0208 ns | 0.0194 ns |
| TestSpanMatch | f | 3.0448 ns | 0.0471 ns | 0.0417 ns |
| TestStringMatch | f | 6.3565 ns | 0.0682 ns | 0.0569 ns |
| TestCharMatch | z | 0.9171 ns | 0.0291 ns | 0.0272 ns |
| TestSpanMatch | z | 2.7847 ns | 0.0321 ns | 0.0284 ns |
| TestStringMatch | z | 5.8219 ns | 0.0443 ns | 0.0370 ns |
可以看出 CharMatch() 始终需要常量时间,而 SpanMatch() 和 StringMatch() 需要根据匹配数量的增加而增加时间。不过与 CharMatch() 的差异并不大,SpanMatch() 明显更快(同时对GC的压力也没有增加),因此对你来说可能已经够用了。
- 问题#2:(加分题)
我在这里违反了标准的哪一部分?
截至C# 14 / .NET 10,问题似乎在于,为了把一个 string 与一个 char 连接起来,首先需要通过 ToString() 将字符转换成字符串,而这在常量表达式中是不允许的。为证实,请从 12.13.5 Addition operator
UTF-16字符串连接:
string operator +(string x, string y); string operator +(string x, object y); string operator +(object x, string y);这些二元+ 运算符的重载执行UTF-16字符串的连接……任何不是ref struct (§16.2.3) 的非字符串操作数,将通过调用从类型
object继承的虚拟方法ToString,被转换为其UTF-16字符串表示。
也就是说,"" + c 实际上被定义为 "" + ((object)c).ToString()。
不幸的是,这种转换似乎并非 12.26常量表达式 中定义的被允许的转换之一:
常量表达式中允许的转换:
- 身份转换
- 数值转换
- 枚举转换
- 常量表达式转换
- 隐式和显式引用转换,前提是转换的源是一个计算为null的常量表达式。
注:其他转换,包括装箱、拆箱,以及对非空值的隐式引用转换,在常量表达式中不被允许。注释结束
这一注释似乎否定了 ToString() 的转换。这并不完全令人意外,因为通常,常量值的 ToString() 转换并不一定是常量自身,可能会受文化影响。因此,例如如果我做如下操作
readonly static int i = -1;
readonly static string istring = "" + i;
那么结果字符串将取决于当前文化,具体参见 CultureInfo.CurrentCulture.NumberFormat.NegativeSign,这并不总是标准的 U+002D HYPHEN-MINUS。例如,在瑞典文化中,它是 U+2212 MINUS SIGN,请参见这里的演示。
至于在插值字符串中使用 const char 值:
const string s = $"{c}";
这在文档页 [The const keyword] 中被明确禁止:
插值字符串在所有使用的表达式也是常量字符串时才可以作为常量。
其中“常量字符串”的定义见 [12.8.3 Interpolated string expressions](强调部分):
常量插值字符串是一个包含
- 没有插值,或者
- 插值的表达式是类型为字符串的常量表达式,并且这些插值没有interpolation_minimum_width、Regular_Interpolation_Format或 Verbatim_Interpolation_Format说明符。
char 不是 string 类型,因此显然被排除了。[1]
- 问题#1:
我该如何真正实现我想要做的事,即在一个const string中嵌入一个const char?*
不幸的是,在C# 14 / .NET 10中似乎没有开箱即用的办法来做到这一点,就像在字符串中嵌入一个 const int 或一个 const double 一样。你可能需要考虑一些变通方法,例如使用 readonly static 字段,或将你的 char 常量设为字符串并将它们的首字符暴露为只读属性:
private const string cstring = "c";
public static char C => cstring[0];
或者,如果你需要在许多地方处理这个问题,正如 [Alexander Petrov] 在评论中所建议的,你也可以考虑创建一个自定义的 C#源生成器 为每个 const char 生成一个相应的 const string。
[1] 当然 char.ToString() 本身当然不是文化相关的,因此从理论上讲Microsoft可以把它加入常量表达式允许构造的列表,或把 char 加入常量插值允许的类型,或者对将字符串和字符相加的 + 运算符增加一个显式覆盖以避免装箱,然后再把它加入到允许的构造列表中。但是,引用Eric Lippert在 这个回答中 的话:
功能默认未实现;C#没有这个功能,因为没有人设计、实现并将该功能交付给用户。
这似乎只是他们尚未实现的一个合理功能。