使用接口
在一开始使用面向接口这个技巧时候更多是感觉用这个比较高级比较酷炫,抱着一些框架代码这样用我也试试的心态用。
用多之后会发现其实还挺有意思,以前在学面向对象时候也会说什么面向接口而不是面向实现写代码,其实一开始都是不知所云。
举个例子,比如我游戏中有一大群实体(Entity),我每帧需要循环更新这些实体和绘制这些实体。有一种思路是用一个抽象基类来处理,还有种思路就是用接口来处理。比如有IUpdatable和IDrawable这两个接口来负责更新和绘制功能,那么我只需要获得全部实现IUpdatable的物体(不管你是实体还是单纯显示功能的背景)然后执行对应接口方法即可。
interface IUpdate{void Update();} interface IDrawable{ void Draw();} public class Entity : IUpdatable, IDrawable { public void Update(){xxx} public void Draw(){xxx} } public class Background: IDrawable { public void Draw(){xxx} }
虽然面向对象的书籍有也经典例子使用基类使用虚函数,但是个人感觉面向接口稍微好点。当然文无定法,看个人习惯。
善用抽象类
刚开始写代码时候我不是很了解抽象类的作用—不能实例化的类有什么作用。后来看了一些框架后才明白,有些时候就是不需要实例化,比如一些基类,我们只需要派生出来。
举个栗子,我们有乌龟兔子和狗狗三种敌人,这些敌人应该继承自基类Enemy,比较常见的做法使用基类:
public class Enemy {} public class Dog : Enemy {} public class Rabbit : Enemy {} public class Tortoise : Enemy {}
但因为你知道敌人本身就是个有点抽象不够具象的概念,所以其实抽象类更加适合:
public abstract class Enemy {} public class Dog : Enemy {} public class Rabbit : Enemy {} public class Tortoise : Enemy {}
使用它的好处在于一定程度可以避免混乱,避免他人和自己乱实例化,当你很久以后再去看你实例化一个Enemy或者Task时候你也不会困惑是什么敌人什么任务。
使用局部变量而不是直接索引
错误的写法
List<Entity> list = {item01, item02,...}; for(int i = 0; i<list.Count; i++) { list[i].Update(); list[i].Draw(); }
正确的写法
List<Entity> list = {item01, item02,...}; for(int i = 0; i<list.Count; i++) { var e = list[i]; e.Update(); e.Draw(); }
使用局部变量缓存一下有助于性能提高,尤其是当你的List特别大时候,随机索引的消耗还是挺大,尤其是热更的解释型语言时候就消耗更大了。
面向数据而不用完全面向数据
一开始写代码时候我也会有种完全数据驱动的冲动,但其实如果完全数据驱动其实会很乱。我见过写Lua时候有同事就把一个Table里面10多20个属性都堆进去,然后美其名曰数据驱动,但这样过几天估计都不知道是啥怎么配置了。
面向数据驱动应该是把真正需要的数据属性提取出来,如果现在设计时候不需要配置就不要臆想着拓展性以后可配置什么的,很多情况下都是徒劳无功,反而留着增加维护成本。
适当暴露错误
在我刚入行时候前辈就告诉我要为了代码健壮性要用到一些判断是否为空的操作
if (entity != null) { do xxx }
但随着经验增加我发现有些时候不能一味兼容错误,而是需要这里把这个错误暴露出来告诉测试人员这里有错误:
Debug.Assert(entity != null, "请查看xx表格有没写错!") if (entity != null) { do xxx }
有些时候如果一味地加保护会把原来的问题隐蔽掉,从而导致后面查错查半天,在该暴露错误时候暴露出来可以降低除错成本。
这五个小技巧就是我这几年写代码觉得比较有用处的地方,希望对你有帮助!
Sadi
2021年11月14日
我有个经验在上线前可以充分暴露错误,能蹦就蹦,在上线后要尽量兼容错误,能不蹦就不蹦。
所以在代码中用宏写两套逻辑比较好
#if shiping...
@anhanjinj:对的!虽然上线后尽量不崩但也需要在日志里上报有问题的地方