String.Replace抛出ArgumentOutOfRangeException:原因与复现方法探究
代码示例
string QueryText = "unknown input"; // 这是一个属性 async Task MethodOneAsync() { // 此处为一些异步代码 if (!string.IsNullOrEmpty(QueryText)) { MethodTwo(); } } void MethodTwo() { // 一些代码 MethodThree(); // 更多代码 } void MethodThree() { string replacedText = QueryText.Replace(",", ""); // 此处抛出异常 }
异常信息
抛出异常:
System.ArgumentOutOfRangeException: Value must be positive. Parameter name: count
完整堆栈跟踪:
StringBuilder.Append (System.String value, System.Int32 startIndex, System.Int32 count) String.ReplaceCore (System.String oldValue, System.String newValue, System.Globalization.CultureInfo culture, System.Globalization.CompareOptions options) String.Replace (System.String oldValue, System.String newValue, System.StringComparison comparisonType)
构建与系统信息
- 项目基于.NET Standard 2.1构建,用于Xamarin
- 仅收到1份Android 13环境下的崩溃报告
已尝试的测试
编写测试函数:
static void Test(string text) { text.Replace(",", ""); }
测试输入:
Test(""); Test(" "); Test(null); // 抛出NullReferenceException
均未触发目标异常。
随后尝试多线程测试:
Task.Run(() => Test(" ")); Task.Run(() => Test("")); Task.Run(() => Test("A")); Task.Run(() => Test("B")); Task.Run(() => Test("C")); Task.Run(() => Test("D")); Task.Run(() => Test("E")); Task.Run(() => Test("F")); Task.Run(() => Test("G")); Task.Run(() => Test(",")); Task.Run(() => Test(" ,")); Task.Run(() => Test(", ")); Task.Run(() => Test("a,")); Task.Run(() => Test("a ,")); Task.Run(() => Test("a, ")); Task.Run(() => Test(",a")); Task.Run(() => Test(" ,a")); Task.Run(() => Test(", a"));
依然未复现异常。
异常原因分析
1. 共享属性的并发修改竞态条件
QueryText作为实例属性,若在多线程环境下被并发修改(比如多个线程同时调用MethodOneAsync,或其他线程直接修改该属性),可能触发string.Replace内部实现的竞态问题。虽然string本身是不可变类型,但Xamarin的Mono运行时对Replace的实现可能依赖字符串的内部状态,当属性引用被异步切换时,会导致计算出的count参数非法。
2. Xamarin Android特定的字符串损坏场景
该异常仅出现在Android 13环境中,大概率和原生交互有关:如果QueryText的值来自Android原生API(如Intent、SharedPreferences、JNI调用),当原生层传递的字符串存在损坏(比如长度字段与实际字符数据不匹配),托管层的string实例内部状态会不一致。调用Replace时,内部StringBuilder.Append方法会根据字符串的Length计算参数,若实际可用字符数小于计算值,就会抛出ArgumentOutOfRangeException。
3. .NET Standard实现的偶发bug
Xamarin的Mono运行时对.NET Standard 2.1的string.Replace实现可能存在特殊场景下的bug,比如处理包含特殊Unicode字符、空字符或超长字符串时,会触发参数计算错误。
复现方法
1. 构造内部状态损坏的字符串
通过反射创建长度与实际字符数组不匹配的string实例,模拟原生层传递的损坏字符串:
using System.Reflection; // 仅用于测试的不安全代码 char[] charArray = new char[] { 'x', ',', 'y' }; // 构造时传入的长度(5)远大于字符数组实际长度(3) string corruptedString = (string)typeof(string) .GetConstructor( BindingFlags.NonPublic | BindingFlags.Instance, null, new Type[] { typeof(char[]), typeof(int), typeof(int) }, null ) .Invoke(new object[] { charArray, 0, 5 }); corruptedString.Replace(",", ""); // 会抛出目标异常
2. 结合Xamarin Android原生交互
在Android项目中,通过JNI手动构造长度不匹配的原生字符串,传递到托管层后赋值给QueryText,再调用MethodThree。
3. 高频并发修改+特殊输入
提升并发修改的频率,并加入特殊字符输入,模拟极端场景:
// 高频并发修改QueryText并执行替换操作 for (int i = 0; i < 2000; i++) { Task.Run(() => { // 随机生成包含大量逗号的字符串 QueryText = new string(',', new Random().Next(0, 2000)); MethodOneAsync().Wait(); }); }
内容的提问来源于stack exchange,提问作者Tomáš Aresak Malčánek

