如何写出优雅漂亮的c#代码_方法(三)
今天我们来讲如何把方法写的干净漂亮,其实方法写好并不难,主要注意以下几点。
- 避免过多的参数
- 避免过长的方法体
- 正确的访问级
- 合理的定义返回值
避免过多的参数
参数过多会导致程序代码冗长,不仅仅调用方会蒙圈。如果方法参数有修改更是一件很痛苦的事情。
例:
定义一个方法用来保存药品,药品包括“品名,规格,生产厂商,适应症,用法用量,注意事 项,不良反应,有效期,有效期至,批次,批准文号”,等信息。(此处仅作为举例,真正的药品属性要比这多得多,此处不一一列举)
经验不足的小伙伴很容易把方法写成这样。
/// <summary> /// 添加药品 /// </summary> /// <param name="name">名称</param> /// <param name="spec">规格</param> /// <param name="manufacturer">生产厂商</param> /// <param name="indication">适应症</param> /// <param name="usageDosage">用法用量</param> /// <param name="mattersNeedingAttention">注意事项</param> /// <param name="untowardEffect">不良反映</param> /// <param name="expiryDate">有效期</param> /// <param name="vld">有效期至</param> /// <param name="batchNumber">批次</param> /// <param name="LicenceNo">批准文号</param> /// <returns></returns> private static bool AddDrug( string name, string spec, string manufacturer, string indication, string usageDosage, string mattersNeedingAttention, string untowardEffect, int expiryDate, DateTime vld, string batchNumber, string LicenceNo) { return true; }
虽然做法没什么问题,参数名称很清楚,还加了相应的注释。但真正调用方法并传参的时候,你会发现这种方法并不友好。
这么多的参数,每一个对应的写起来,真的很痛苦。如果有一个参数传错了,你需要检查所有的参数,假设这个方法有30个参数,检查参数所耗费的时间精力就会翻倍,而这种写法的缺点就是很容易造成传参错误,当然你依然可以用以下方式调用,但这并不是我们最推荐的方法。
static void Main(string[] args) { string name = "伟哥"; string spec = ""; string manufacturer = ""; string indication = "治疗男子**勃起功能障碍"; string usageDosage = ""; string mattersNeedingAttention = ""; string untowardEffect = ""; int expiryDate = 1; string batchNumber = ""; string LicenceNo = ""; DateTime vld = new DateTime(2021, 1, 10); AddDrug( name: name, spec: spec, manufacturer: manufacturer, indication: indication, usageDosage: usageDosage, mattersNeedingAttention: mattersNeedingAttention, untowardEffect: untowardEffect, expiryDate: expiryDate, vld: vld, batchNumber: batchNumber, LicenceNo: LicenceNo ); }
在这里我推荐如果方法参数超过5个一定要写成类,有以下几点好处。
- 不会造成方法参数的冗长,可读性高
- 修改方法参数,只要修改对应类中的属性即可,不会造成编译错误。
- 参数类的构造函数,get;set;属性还可以对参数进行初步的验证。
static void Main(string[] args) { Drug drug = new Drug(); drug.name = "伟哥"; drug.spec = ""; drug.manufacturer = ""; drug.indication = "治疗男子**勃起功能障碍"; drug.usageDosage = ""; drug.mattersNeedingAttention = ""; drug.untowardEffect = ""; drug.expiryDate = 1; drug.batchNumber = ""; drug.LicenceNo = ""; drug.vld = new DateTime(2021, 1, 10); AddDrug(drug); } private static bool AddDrug(Drug drug) { return true; }
避免过长的方法体
例:
将Drug类序列化为xml,并保存到本地磁盘。
//以下代码仅作为逻辑讲解使用,并未真正调试并运行成功,请见谅 static void Main(string[] args) { Drug drug = new Drug(); drug.name = "伟哥"; drug.spec = ""; drug.manufacturer = ""; drug.indication = "治疗男子**勃起功能障碍"; drug.usageDosage = ""; drug.mattersNeedingAttention = ""; drug.untowardEffect = ""; drug.expiryDate = 1; drug.batchNumber = ""; drug.LicenceNo = ""; drug.vld = new DateTime(2021, 1, 10); string str = string.Empty; try { MemoryStream Stream = new MemoryStream(); XmlSerializer xml = new XmlSerializer(typeof(Drug)); //序列化对象 xml.Serialize(Stream, drug); Stream.Position = 0; StreamReader sr = new StreamReader(Stream); str = sr.ReadToEnd(); sr.Dispose(); Stream.Dispose(); } catch (Exception ex) { Console.WriteLine("序列化失败"); return; } try { FileStream fs = new FileStream(@"D:\drug\drug.xml", FileMode.OpenOrCreate, FileAccess.ReadWrite); StreamWriter sw = new StreamWriter(fs); if (sw != null) { sw.WriteLine(str); sw.Close(); sw.Dispose(); } if (fs != null) { fs.Close(); fs.Dispose(); } } catch (Exception) { Console.WriteLine("文件保存失败"); 4000 } }
经验不足小伙伴经常写出这种代码,导致方法体冗长。
接下来我们具体分析以下,本方法主要包含三个主要逻辑
- 创建Drug类
- 序列化drug
- 创建本地xml
按照这三个功能点,我们继续优化。
static void Main(string[] args) { //创建drug //我们需要的是一个Drug的实体,具体drug怎么来的main方法并不关系,直接交给CreateDrug就够了 Drug drug = CreateDrug(); try { //生成xml //在一个完整的项目中不可能只有一个类需要序列化为xml,所以我们要定义的通用些 string xml = GenerateXml(typeof(Drug), drug); //保存xml //如何保存,用那种方式保存,io如何操作,文件流创建,关闭,销毁,对main来说并不关心, //main只需要保存方法,其他的问题是SaveXml方法的事情 SaveXml(xml, @"D:\drug\drug.xml"); } //这个异常处理并不完善,后续我们讲详细讲述如何玩好try catch catch { } } private static Drug CreateDrug() { Drug drug = new Drug(); drug.name = "伟哥"; drug.spec = ""; drug.manufacturer = ""; drug.indication = "治疗男子**勃起功能障碍"; drug.usageDosage = ""; drug.mattersNeedingAttention = ""; drug.untowardEffect = ""; drug.expiryDate = 1; drug.batchNumber = ""; drug.LicenceNo = ""; drug.vld = new DateTime(2021, 1, 10); return drug; } private static string GenerateXml(Type t, object model) { string str = string.Empty; try { MemoryStream Stream = new MemoryStream(); XmlSerializer xml = new XmlSerializer(t); //序列化对象 xml.Serialize(Stream, model); Stream.Position = 0; StreamReader sr = new StreamReader(Stream); str = sr.ReadToEnd(); if (sr != null) { sr.Dispose(); Stream.Dispose(); } } catch (Exception ex) { throw; } return str; } private static void SaveXml(string xml, string path) { try { FileStream fs = new FileStream(path, FileMode.OpenOrCreate, FileAccess.ReadWrite); StreamWriter sw = new StreamWriter(fs); if (sw != null) { sw.WriteLine(xml); sw.Close(); sw.Dispose(); } if (fs != null) { fs.Close(); fs.Dispose(); } } catch (Exception) { throw; } }
我们看到了,main方法中干净了不少。main只负责调用,而不去管具体的实现逻辑,实现逻辑交给三个子方法,这也就意味着,逻辑变动不会影响到调用,程序的可维护性大大提高。
所以,一个方法的逻辑内容超过50行那一定需要优化了(这里强调下,不是说不超过50就不需要优化,将不同的逻辑内容拆成不同的逻辑方法是个非常好的习惯),可以按照以上逻辑,分功能拆出不同的子逻辑,大大提高了可读性,也提高了可维护性,何乐而不为。
正确的访问级
例:
定义Car类,Car是一辆可以自动驾驶的汽车,Car需要对外公开Start(),Run(),Stop()方法供外界调用,同时Satrt,Run,Stop的内部逻辑也要实现。
public class Car { /// <summary> /// 启动 /// </summary> public void Start() { Electrify(); StarterRun(); CarburetorRun(); EngineRun(); //启动完成,随时可以狂奔 } /// <summary> /// 行驶 /// </summary> public void Run() { SteppingOnClutch(); HangingGear(); Throttle(); //2倍音速狂奔中 } /// <summary> ///停车 /// </summary> public void Stop() { Brake(); Stall(); //这颗难以压抑的心也需要熄火 } /// <summary> /// 通电 /// </summary> public void Electrify() { } /// <summary> /// 起动机工作 /// </summary> public void StarterRun() { } /// <summary> /// 化油器工作 /// </summary> public void CarburetorRun() { } /// <summary> /// 引擎工作 /// </summary> public void EngineRun() { } /// <summary> /// 离合器 /// </summary> public void SteppingOnClutch() { } /// <summary> /// 挂挡 /// </summary> public void HangingGear() { } /// <summary> /// 油门 /// </summary> public void Throttle() { } /// <summary> /// 刹车 /// </summary> public void Brake() { } /// <summary> /// 熄火 /// </summary> public void Stall() { } }
好了,我们已经实现了。可以看到,基本上每一个步骤我们都实现了,接下来我们是不是可以愉快的让别人去开了,但是问题来了。
f**k,你不是一个很牛B的AI自动驾驶吗,挂挡、刹车、离合、甚至化油器启动机都要我自己操作,这个自动驾驶不过如此、、、于是出现了以下代码。
static void Main(string[] args) { Car aNBCar = new Car(); aNBCar.Electrify(); aNBCar.StarterRun(); aNBCar.CarburetorRun(); aNBCar.EngineRun(); //启动完成,随时可以狂奔 aNBCar.SteppingOnClutch(); aNBCar.HangingGear(); aNBCar.Throttle(); //2倍音速狂奔中 aNBCar.Brake(); aNBCar.Stall(); //这颗难以压抑的心也需要熄火 }
这时你会说:你只要直接Start(),就好了。
f**k,既然start能搞定,你给我这么多东西干啥?
因为访问级别的不明确导致重复的逻辑,重复的代码,必然导致代码的冗余。public 只有对外公开时采用,正确的控制访问级,根据实际情况选择访问级别。
public class Car { /// <summary> /// 启动 /// </summary> public void Start() { Electrify(); StarterRun(); CarburetorRun(); EngineRun(); //启动完成,随时可以狂奔 } /// <summary> /// 行驶 /// </summary> public void Run() { SteppingOnClutch(); HangingGear(); Throttle(); //2倍音速狂奔中 } /// <summary> ///停车 /// </summary> public void Stop() { Brake(); Stall(); //这颗难以压抑的心也需要熄火 } /// <summary> /// 通电 /// </summary> private void Electrify() { } /// <summary> /// 起动机工作 /// </summary> private void StarterRun() { } /// <summary> /// 化油器工作 /// </summary> private void CarburetorRun() { } /// <summary> /// 引擎工作 /// </summary> private void EngineRun() { } /// <summary> /// 离合器 /// </summary> private void SteppingOnClutch() { } /// <summary> /// 挂挡 /// </summary> private void HangingGear() { } /// <summary> /// 油门 /// </summary> private void Throttle() { } /// <summary> /// 刹车 /// </summary> private void Brake() { } /// <summary> /// 熄火 /// </summary> private void Stall() { } }
启动、挂挡、踩离合、给油门这种事就不要让外人掺和了。
好的,伙计,我知道该咋办了。
static void Main(string[] args) { Car aNBCar = new Car(); aNBCar.Start(); aNBCar.Run(); aNBCar.Stop(); }
正确的定义访问级,不要让你的基友误解,使逻辑更清晰。
合理的定义返回值
例:
写一个CreateOrder方法,要求返回订单的Guid id,string order_no。
首先我们知道c#并不支持多返回值,所以经常有新人这样写。
public static string CreateOrder() { return "0123456789|EAB4B190-5784-467A-B26B-4B77FD4890AD"; } static void Main(string[] args) { string order = CreateOrder(); string order_no = string.Empty; Guid order_id = Guid.Empty; //没有办法,既然是返回一定格式的字符串,那就只能先对字符串效验 if (string.IsNullOrWhiteSpace(order) && order.Contains('|')) { //分离成为数组 var ary = order.Split('|'); order_no = args[0]; string id = ary[1]; //为确保数据的合法性,还要对数据进行二次效验 if (Guid.TryParse(id, out order_id)) { } } }
大家看到了,仅仅是取到返回值就写了这么多代码,这些代码完全可以看做在浪费时间,我们看下一种方法。
static void Main(string[] args) { Guid id = Guid.Empty; string order_no = string.Empty; CreateOrder(ref id, ref order_no); } public static void CreateOrder(ref Guid id, ref string order_no) { id = new Guid(); order_no = "0123456789"; }
OK,好的,没有了那么冗长的代码。但还有其他问题,如果要求返回订单金额怎么办?继续加ref?没有问题,但是每加一个ref就是增加一个方法参数,都会导致一次编译报错。这在团队开发是很不利的,我们继续优化。
static void Main(string[] args) { KeyValuePair<Guid, string> order = CreateOrder(); Guid id = order.Key; string order_no = order.Value; } public static KeyValuePair<Guid, string> CreateOrder() { return new KeyValuePair<Guid, string>(Guid.NewGuid(), "0123456789"); }
这样的确不会导致参数变化了,但是并没有根本的解决增加返回值的问题,KeyValuePair只针对一对一的情况,如果这样呢。
static void Main(string[] args) { string order_no = string.Empty; Guid id = Guid.Empty; decimal amount = 0m; string[] order = CreateOrder(); if (order.Length == 3) { return; } if (string.IsNullOrEmpty(order[0])) { order_no = order[0]; } if (string.IsNullOrWhiteSpace(order[1])) { if (Guid.TryParse(order[0], out id)) { } } if (decimal.TryParse(order[2], out amount)) { } } public static string[] CreateOrder() { return new string[] { "0123456789", "EAB4B190-5784-467A-B26B-4B77FD4890AD" }; }
解决了多返回值的问题,但是又需要一大坨代码对返回值进行效验,我们需要的是优雅漂亮的代码,不妥不妥啊,我们还可以使用c# 的元组。
static void Main(string[] args) { Tuple<Guid, string,decimal> order = CreateOrder(); Guid id = order.Item1; string order_no = order.Item2; decimal amount = order.Item3; } public static Tuple<Guid, string, decimal> CreateOrder() { return new Tuple<Guid, string, decimal>(Guid.NewGuid(), "0123456789", 11.5m); }
看上去已经很不错了,代码简单逻辑清晰。在c# 7.0中,甚至可以这样做。
static void Main(string[] args) { var (id, order_no, amount) = CreateOrder(); } public static (Guid, string, decimal) CreateOrder() { return (Guid.NewGuid(), "0123456789", 11.5m); }
这、这、这,这不就是我们要的如诗一般优美的代码么,但是缺点依然有,如果我们要求返回订单所有属性,共计100个,是不是有点坑,这时应该这样做。
public class Order { public Guid id { get; set; } public string order_no { get; set; } } static void Main(string[] args) { Order order = CreateOrder(); } public static Order CreateOrder() { return new Order(); }
好的,我们似乎一劳永逸了,哪怕有200个属性我们都不怕了。但是也有缺点,不分青红皂白的拿类做返回值,容易造成类文件爆炸的问题。
总结:
- 避免过多的参数
不要给方法定义太多的参数,如果真的需要很多参数,可以考虑使用类作为参数 - 避免过长的方法体
方法的内容不要过长,根据方法内部的逻辑模块拆分成对应的子方法 - 正确的访问级
该公开的公开,该封闭的封闭,以免引起歧义 - 合理的定义返回值
如果返回值是一个键值对,一对一,用KeyValuePair最合适不过了
如果返回值只有三两个使用元组最好,推荐使用c# 7.0版本写法
如果返回值很多推荐使用类返回
最后感谢大家,下一期我们介绍如何写class
- 国际:写出漂亮代码的七种方法----看了美化化码的想法,觉得很好,故加以引用,希望更多人可以看到
- 20090104――写出漂亮代码的七种方法_笔记
- 论如何写出优雅的Android代码--------ActivityManager
- 写出漂亮代码的七种方法
- C# 2.0:使用匿名方法、迭代程序和局部类来创建优雅的代码
- 代码之美 - 如何写出优雅的PHP代码
- 程序员如何写出优雅的代码?
- 写出漂亮代码的几种方法
- 写出漂亮代码的七种方法
- 写出优雅简明代码的论题集 -- Csharp(C#)篇[1]
- 代码之美 - 如何写出优雅的PHP代码
- 如何写出优雅兼备可读性的javascript代码
- 如何在Word中排版出漂亮的编程语言代码样式?(较好用的部分方法汇总)
- 如何写出优雅的 Golang 代码
- 写出漂亮代码的七种方法
- 写出漂亮代码的七种方法
- 写出漂亮代码的七种方法
- 写出优雅简明代码的论题集 -- Csharp(C#)篇[1]
- 如何写出漂亮的js代码(转载)
- 写出优雅简明代码的论题集 -- Csharp(C#)篇[2]