C#异步语法糖的苦与甜


C#异步语法糖的苦与甜


我们项目的Unity由于使用了CSharp70Support插件,支持到C#7.0的语法,所以也是支持await/async异步语法糖的,因此在客户端代码中大量使用了异步语法,所有的客服端和服务器数据同步都是由异步函数完成的.但是在实际的使用中,我觉得大量使用异步语法并不是一个好的编程实践,有些情况下引入异步之后,反而增加了函数的复杂度.

客户端在链接服务器之后,服务器只会下发玩家个人模块的数据,而游戏里其他功能模块的数据,此时客户端仍是没有的,这些数据是玩家打开对应的功能时才会异步进行请求对应模块的数据,基本的流程如下所示:

所以逻辑模块中,有大量的异步函数,类似这样的:

      // 获取npc聊天数据
        public async ValueTask GetNpcChatInfos(int npcId)
        {
            if (_npcChats == null)
                _npcChats = new Dictionary();

            if (!_npcChats.ContainsKey(npcId))
            {
                var msg = (GetNpcChatInfoP) await Gate.SendAsync(new GetNpcChatInfoQ());
                foreach (var info in msg.Infos)
                    _npcChats[info.NpcId] = info;
            }

            _npcChats.TryGetValue(npcId, out var chatInfo);
            return chatInfo;
        }

在UI上面要使用到逻辑数据的时候就调用这样的函数去获取,因而问题就出现了:因为这是一个异步函数,所以在UI代码中调用此函数的函数也会是个异步函数,但是大多数情况下UI代码都应该是顺序执行的,并不需要异步去等待某个操作完成,而引入一个异步函数无疑是增加了问题的复杂性!

如下代码,这是调用上面函数的UI代码,这是一个初始化和NPC聊天剧情文本的函数,先从获取逻辑数据,得到当前玩家和此NPC的聊天进度,然后根据进度对应的剧情文本和节点来生成以往的对话记录,最后显示记录并从当前节点开始进行UI表现.在这个函数中有两处用到了异步调用的结果,一个是SwitchToDialog,另一个是InitRecords,事实上我原不应该向这二个函数传参数(参数越少,函数越好的编程观念= =),我可以在这二个函数中再次调用异步方法来获取数据,但是这样会打乱整个函数的执行顺序,除非将这二个函数改成返回Task并await,将二个原本不是异步的函数改成异步函数,无疑是增加了代码的复杂度,这是得不偿失的改动.但是假如GetNpcChatInfos不是异步函数,则可践行参数越少,函数越好的理论.

        private async void InitDialogData()
        {
            var info   = await Player.Instance.Chat.GetNpcChatInfos(_npcId);
            var dialog = info.Tree;
            var node   = info.NodeId;
            SwitchToDialog(dialog, node);
            // 如果对话记录为空,初始话此对话文本的聊天记录
            if (_chatRecords.IsNullOrEmpty())
                InitRecords(info);

            DisplayNewLoopListItem();
            ActCurrentNode();
        }

除此之外,UI上可能还存在取消操作,假如存在这样的情况:玩家在打开某个界面后立即关闭,但异步操作还在等待逻辑数据返回,这样关闭界面后异步的后续操作还要手动取消?这无疑是挖坑埋自己,如果出现Bug,改起来岂不是很爽歪歪?当然事实上应该是不会出现这样的情况的,一般进行客户端进行同步操作的时候都会阻止互动.

倒是在逻辑中遇到代码执行顺序的问题,比如在一个比较复杂的异步函数中,有时候会执行异步语句,有时候不会执行异步语句,那么此函数极有可能会出现执行顺序的问题.如果此函数返回的是void或者调用它的函数并没有await它的Task,那么短时间内调用此函数多次,此函数异步语句后的代码是可能调用顺序不同的,即有可能是第二次调用的异步语句后的代码先执行,这有时候和代码想表达意思是不一致的!并且写代码的时候容易忽视掉!比如下面函数,如果第一次调用的TestKey没有,它会异步获取.如果这时候第二次调用已开始,并且TestKey已存在,那么会先执行完最后的语句,这样MyList的元素排序就和原本预期会不一致!

        private async void FuncAsync()
        {

            if (!ResultDict.Contains(TestKey))
            {
               var TestResult = await GetResultAsync();
               ResultDict.Add(TestKey,TestResult);
            }
            
            MyList.Add(ResultDict[TestKey])
        }

我们知道,await/async属于编译器魔法,异步函数在编译时会被编译成一个状态机,异步只是在当前线程上分了一段时间来做其他事情并且不会阻塞当前线程,异步虽然好用,但是并没有让复杂的问题变简单,只是让回调这种方式变得更优雅而已,通过编译器帮助你将回调变成异步完成后操作的方式.所以异步的语法虽然很好用,但是使用时应该慎重考量下,是否确实有大提升,否则能用回调的情况下,还是多用回调应该会好些.