调用外部ApiCall()时MoveNext()中断致循环终止问题排查
问题:调用ApiCall()后循环中断,不调用则正常执行
我是C#新手,现有代码在不调用ApiCall()函数时,能正常遍历并打印每个query字符串;但调用该函数后,while循环仅执行一次就中断,根本到不了"doc-dispose"的输出。我搞不懂为什么一个没有返回值的方法会影响调用方的循环。
主代码:
using(StreamReader sr = new StreamReader($"Nebraska.json")) { List<string> listOfParks = new List<string>(); string? line; while((line = sr.ReadLine()) != null) { using JsonDocument documentSplitByLine = JsonDocument.Parse(line); JsonElement root = documentSplitByLine.RootElement; using(JsonElement.ArrayEnumerator parks = root.EnumerateArray()) { while(parks.MoveNext()) { string name = parks.Current.GetProperty("name").ToString(); string local = parks.Current.GetProperty("location").ToString(); string state = parks.Current.GetProperty("state").ToString(); string thing = ""; if(local.Contains(",")) { thing = local.Substring(0, local.IndexOf(',')); } else if(local.Contains(" and ")) { thing = local.Substring(0, local.IndexOf(" and ")); } else if(local.Contains("-")) { thing = local.Substring(0, local.IndexOf("-")); } else if(local.Contains("(")) { thing = local.Substring(0, local.IndexOf("(")); } else if(name.Contains("(")) { name = name.Substring(0, name.IndexOf("(")); } else if(name.Contains(" -")) { name = name.Substring(0, name.IndexOf(" -")); } else if(local.All(char.IsDigit)) { thing = local.Substring(0, local.First(char.IsDigit)); } else if(local.Contains("/")) { thing = local.Substring(0, local.IndexOf("/")); } else{ thing = local; } string query = name+"%20"+thing+"%20"+state; Console.WriteLine(query); // 调用API时出现问题,注释掉这段代码循环正常 ApiCall(query); } Console.WriteLine("doc-dispose"); documentSplitByLine.Dispose(); } } }
ApiCall()函数实现:
static void ApiCall (string query) { HttpClient client = new HttpClient(); string baseAdd = "https://maps.googleapis.com/maps/api/place/findplacefromtext/json?input="; string tagAdd = "&inputtype=textquery&fields=formatted_address%2Cname%2Crating%2Cgeometry&key=AIzaSyBr2e34qryWOxc5VrAO6xfod2SboB6mhhc"; string finalQuery = baseAdd+query+tagAdd; Console.WriteLine("Calling web API..."); var responseTask = client.GetAsync(finalQuery); responseTask.Wait(); if(responseTask.IsCompleted) { var result = responseTask.Result; if(result.IsSuccessStatusCode) { var messageTask = result.Content.ReadAsStringAsync(); messageTask.Wait(); JsonDocument documentSplitByLine = JsonDocument.Parse(messageTask.Result); JsonElement root = documentSplitByLine.RootElement; var parks = root.EnumerateObject(); while(parks.MoveNext()) { var park = parks.Current; var thing = park.Value[0]; string name = "" +thing.GetProperty("name"); string lat = "" +thing.GetProperty("geometry").GetProperty("location").GetProperty("lat"); string lng = "" +thing.GetProperty("geometry").GetProperty("location").GetProperty("lng"); string formatted_address = "" +thing.GetProperty("formatted_address"); LatLng thisLatLng = new LatLng(float.Parse(lat), float.Parse(lng)); StatePark thisPark = new StatePark(name, thisLatLng, formatted_address); thisPark.SayName(); } } else { Console.WriteLine("failed response"); } } }
解决方案及原因分析
核心问题
循环中断是因为ApiCall()中的代码抛出了未捕获的异常,直接终止了程序执行。不调用ApiCall()时没有异常,所以循环正常。具体触发异常的原因有以下几点:
每次新建
HttpClient耗尽套接字资源HttpClient是设计用来复用的,每次调用都新建实例会快速耗尽系统套接字,导致后续请求失败抛出异常。用
.Wait()同步等待异步任务易引发死锁或未处理异常.Wait()会阻塞当前线程,如果API请求过程中出现网络超时、JSON解析错误等情况,未捕获的异常会直接终止程序。JsonDocument未释放导致资源泄漏ApiCall()中解析JSON的JsonDocument没有用using包裹,长期运行会占用大量内存,间接引发异常。
修改后的代码
1. 重构ApiCall()为异步方法,复用HttpClient
// 全局复用HttpClient,不要每次调用都新建 private static readonly HttpClient _httpClient = new HttpClient(); static async Task ApiCall(string query) { string baseAdd = "https://maps.googleapis.com/maps/api/place/findplacefromtext/json?input="; string tagAdd = "&inputtype=textquery&fields=formatted_address%2Cname%2Crating%2Cgeometry&key=AIzaSyBr2e34qryWOxc5VrAO6xfod2SboB6mhhc"; string finalQuery = baseAdd + query + tagAdd; Console.WriteLine("Calling web API..."); try { // 用await替代Wait(),避免阻塞和死锁 var result = await _httpClient.GetAsync(finalQuery); // 非成功状态码直接抛出异常,进入catch块处理 result.EnsureSuccessStatusCode(); var responseContent = await result.Content.ReadAsStringAsync(); // 用using包裹JsonDocument,自动释放资源 using JsonDocument documentSplitByLine = JsonDocument.Parse(responseContent); JsonElement root = documentSplitByLine.RootElement; var parks = root.EnumerateObject(); while(parks.MoveNext()) { var park = parks.Current; // 先判断是否存在索引0的元素,避免索引越界异常 if (!park.Value.TryGetElement(0, out var thing)) { Console.WriteLine($"API返回结构异常:{park.Name}下无第0个元素"); continue; } // 安全获取属性,避免属性不存在抛出异常 if (!thing.TryGetProperty("name", out var nameElem)) { Console.WriteLine("缺少name属性"); continue; } string name = nameElem.ToString(); // 解析经纬度用TryParse,避免格式错误导致异常 if (!thing.TryGetProperty("geometry", out var geometryElem) || !geometryElem.TryGetProperty("location", out var locationElem) || !locationElem.TryGetProperty("lat", out var latElem) || !float.TryParse(latElem.ToString(), out float lat) || !locationElem.TryGetProperty("lng", out var lngElem) || !float.TryParse(lngElem.ToString(), out float lng)) { Console.WriteLine("经纬度解析失败"); continue; } string formatted_address = thing.GetProperty("formatted_address").ToString(); LatLng thisLatLng = new LatLng(lat, lng); StatePark thisPark = new StatePark(name, thisLatLng, formatted_address); thisPark.SayName(); } } catch (Exception ex) { // 捕获所有异常,打印信息后继续执行,不会终止循环 Console.WriteLine($"API调用出错:{ex.Message}"); } }
2. 修改主方法为异步,调整循环内的代码
// 主方法改为async Task,支持await调用 static async Task Main(string[] args) { using(StreamReader sr = new StreamReader($"Nebraska.json")) { List<string> listOfParks = new List<string>(); string? line; while((line = sr.ReadLine()) != null) { using JsonDocument documentSplitByLine = JsonDocument.Parse(line); JsonElement root = documentSplitByLine.RootElement; // JsonElement.ArrayEnumerator不需要using,它不实现IDisposable var parks = root.EnumerateArray(); while(parks.MoveNext()) { string name = parks.Current.GetProperty("name").ToString(); string local = parks.Current.GetProperty("location").ToString(); string state = parks.Current.GetProperty("state").ToString(); string thing = local; if(local.Contains(",")) { thing = local.Substring(0, local.IndexOf(',')); } else if(local.Contains(" and ")) { thing = local.Substring(0, local.IndexOf(" and ")); } else if(local.Contains("-")) { thing = local.Substring(0, local.IndexOf("-")); } else if(local.Contains("(")) { thing = local.Substring(0, local.IndexOf("(")); } else if(name.Contains("(")) { name = name.Substring(0, name.IndexOf("(")); } else if(name.Contains(" -")) { name = name.Substring(0, name.IndexOf(" -")); } else if(local.All(char.IsDigit)) { // 原逻辑有问题:如果local全是数字,Substring(0, local.First(char.IsDigit))会得到空字符串,改为去除开头的数字 thing = new string(local.SkipWhile(char.IsDigit).ToArray()); } else if(local.Contains("/")) { thing = local.Substring(0, local.IndexOf("/")); } // 用字符串插值更清晰 string query = $"{name}%20{thing}%20{state}"; Console.WriteLine(query); // 异步调用API,用await等待完成 await ApiCall(query); } Console.WriteLine("doc-dispose"); // using块会自动释放documentSplitByLine,不需要手动调用Dispose() } } }
关键修改说明
- 复用
HttpClient:全局只创建一个实例,避免套接字耗尽问题。 - 异步/await替代
.Wait():避免死锁,同时符合C#异步编程规范。 - 异常捕获:API调用或解析出错时,捕获异常并打印,程序不会终止,循环可以继续执行。
- 安全解析JSON:用
TryGetElement、TryGetProperty、TryParse等方法,避免因数据格式异常导致程序崩溃。 - 移除多余的
using和Dispose():JsonElement.ArrayEnumerator不需要using,using包裹的JsonDocument会自动释放。
内容的提问来源于stack exchange,提问作者Blake Hunter
相关产品推荐
相关产品推荐

