编写高质量代码改善C#程序的157个建议[动态数组、循环遍历、对象集合初始化]
前言
软件开发过程中,不可避免会用到集合,C#中的集合表现为数组和若干集合类。不管是数组还是集合类,它们都有各自的优缺点。如何使用好集合是我们在开发过程中必须掌握的技巧。不要小看这些技巧,一旦在开发中使用了错误的集合或针对集合的方法,应用程序将会背离你的预想而运行。
本文已更新至http://www.cnblogs.com/aehyok/p/3624579.html 。本文主要学习记录以下内容:
建议16、元素数量可变的情况下不应使用数组
建议17、在多数情况下使用foreach进行循环遍历
建议18、foreach不能代替for
建议19、使用更有效的对象和集合初始化
建议16、元素数量可变的情况下不应使用数组
在C#中,数组一旦被创建,长度就不能改变。如果我们需要一个动态且可变长度的集合,就应该使用ArrayList或List<T>来创建。而数组本身,尤其是一维数组,在遇到要求高效率的算法时,则会专门被优化以提升其效率。一维数组也成为向量,其性能是最佳的,在IL中使用了专门的指令来处理它们。
从内存使用的角度来讲,数组具有以下特点:
1、数组在创建时被分配了一段固定长度的内存。
2、如果数组元素是值类型,则每个元素的长度等于相应的值类型的长度
3、如果数组的元素是引用类型,则每个元素的长度为该引用类型的IntPtr.Size。
4、数组的存储结构一旦被分配,就不能再变化。
而ArryaList是这样的:
1、ArrayList是链表结构,可以动态增减内存空间。
2、如果ArrayList存储的是值类型,则会为每个元素增加12字节的空间,其中4字节用于对象引用,8字节是元素装箱时引入的对象头。
而List<T>是ArrayList的泛型实现,它省去了拆箱和装箱带来的开销。
如果一定要动态改变数组的长度,一种方法是将数组转换为ArrayList或List<T>,如下面的代码所示:
///定义一个一维数组 int[] iArr = { 0,1,3,4,6,7,9}; ///将数组转换为ArrayList ArrayList arrayListInt = ArrayList.Adapter(iArr); arrayListInt.Add(11); ///将数组转换为List<T> List<int> listInt = iArr.ToList<int>(); listInt.Add(11);
还有一种方法是用数组的复制功能。数组继承自System.Array,抽象类System.Array提供了一些有用的实现方法,其中就包含了Copy方法,它负责将一个数组的内容复制到另外一个数组中。无论是哪种方法,改变数组长度就相当于重新创建了一个数组对象。
为了让数组看上去本身就具有动态改变长度的功能,还可以创建一个名为ReSize的扩展方法。
public static class ClassForExtensions { public static Array ReSize(this Array array,int newSize) { Type t = array.GetType().GetElementType(); Array newArray = Array.CreateInstance(t, newSize); Array.Copy(array, 0, newArray, 0, Math.Min(array.Length, newSize)); return newArray; } }
调用方式如下:
static void Main(string[] args) { int[] iArr = { 0,1,3,4,6,7,9}; iArr = (int[])ClassForExtensions.ReSize(iArr, 20); Console.ReadLine(); }
下面我们来对比一下性能,先来看代码:
class Program { static void Main(string[] args) { ResizeArray(); ResizeList(); Console.ReadLine(); } public static void ResizeArray() { int[] iArr = {0,1,3,4,6,8 }; Stopwatch watch = new Stopwatch(); watch.Start();///用于测量时间间隔 iArr = (int[])iArr.ReSize(10); watch.Stop();/// Console.WriteLine("ResizeArray:{0}", watch.Elapsed); } public static void ResizeList() { List<int> iArr = new List<int>(new int[] { 0, 1, 3, 4, 6, 8, 9 }); Stopwatch watch = new Stopwatch(); watch.Start(); iArr.Add(0); iArr.Add(0); iArr.Add(0); watch.Stop(); Console.WriteLine("ResizeList:{0}", watch.Elapsed); } }
Main函数中主要是调用,自己定义的两个方法,第一个是重新设置数组的长度,第二个是设置List<T>的长度,通过运行时间进行测量:
严格意义上讲,List<T>不存在改变长度的说法,此处主要是来进行对比一下,对List<T>设置长度,并且进行赋值,即便是这样,在时间效率上ResizeList比ResizeArray要高很多很多。
建议17、在多数情况下使用foreach进行循环遍历
这里关于如何针对集合才能使用foreach进行遍历我刚刚写了一篇有关IEnumerable和IEnumerator两个接口的文章,有兴趣的话可以看一下。http://www.cnblogs.com/aehyok/p/3641193.html
感觉使用foreach进行循环遍历,总共有三个好处吧:
1、提供了比较简单、简洁的语法。
2、自动将代码置入try-finally块
3、若类型实现IDispose接口,foreach会在循环结束后自动调用Dispose方法
建议18、foreach不能代替for
foreach存在一个问题是:它不支持循环时对集合进行增删操作。我们来看一下简单的例子:
List<int> list = new List<int>() { 1, 2, 3, 4, 5 }; foreach (int item in list) { list.Remove(item); Console.WriteLine(item.ToString()); } Console.ReadLine();
一起看一下执行结果:
那么下面我们来使用for进行尝试:
List<int> list = new List<int>() { 1, 2, 3, 4, 5 }; for (int i = 0; i < list.Count(); i++) { list.Remove(list[i]); } Console.ReadLine();
进行删除肯定是没问题的。但是要仔细看一下,比如它第一次删除索引0的时候,也就是删除了1,那么它会立即重新调整索引,然后第二次删除的时候,删除的不是2,而是3这个项。那么最终运行完发现还剩余两项
foreach循环使用了迭代器进行集合的遍历,它在FCL提供的迭代器内部维护了一个对集合版本的控制。那么什么是集合版本呢?简单的说,其实它就是一个整型的变量,任何对集合的增删操作都会使版本号加1。foreach循环会调用MoveNext方法来遍历元素,在MoveNext方法内部会进行版本号的检测,一旦检测到版本号有变动,就会抛出InvalidOperationException异常。
如果使用for循环就不会带来这样的问题。for直接使用所引器,它不对集合版本号进行判断,所以不存在因为集合的变动而带来的异常(当然,超出索引长度这种情况除外)。
索引,因为版本检测的缘故,foreach循环并不能带起for循环。
建议19、使用更有效的对象和集合初始化
对象初始化设定项支持可以直接在大括号中对自动实现的属性进行赋值。
class Person { public string Name { get; set; } public int Age { get; set; } } class Program { static void Main(string[] args) { Person person = new Person() { Name = "aehyok", Age = 25 }; Console.ReadLine(); } }
以往只能依靠构造方法传值进去,或者在对象构造完毕后对属性进行赋值。现在这些步骤简化了,初始化设定项实际相当于编译器在对象生成后对属性进行了赋值。
class Person { public string Name { get; set; } public int Age { get; set; } } class Program { static void Main(string[] args) { Person person = new Person() { Name = "Kris", Age = 22 }; List<Person> personList = new List<Person>() { new Person() { Name = "aehyok", Age = 25 }, person, null }; Console.ReadLine(); } }
使用集合的初始化设定项,编译器会在集合对象创建完毕后对集合调用Add方法。上面这段代码展示了如何在初始化语句中创建一个新对象或一个现有对象,以及一个null值。
不过,初始化设定项绝不仅仅是为了对象和集合初始化的方便,它更重要的作用是为LINQ查询中的匿名类型进行属性的初始化。由于LINQ查询返回的集合中匿名类型的属性都是只读的,如果需要为匿名类型属性赋值,或者增加属性,只能通过初始化设定项来进行。初始化设定项还能为属性使用表达式。
来看一段代码:
List<Person> lst = new List<Person>() { new Person(){ Age = 10,Name="Tommy"}, new Person(){ Age = 20,Name="Sammy"} }; var entity = from p in lst select new { p.Name, AgeScope = p.Age > 10 ? "Old" : "Young" }; foreach (var item in entity) { Response.Write(string.Format("name is {0},{1}", item.Name, item.AgeScope)); }
AgeScope 属性是经过计算得出的,有了如此方便的初始化方式,使得代码更加优雅灵活。
(原创)PetaPoco使用小记(后续会继续补充)
接触PetaPoco已经有一段时间了,为了全面了解一下PetaPoco,刚好结合目前在做的一个项目,对常用的几个业务操作用PetaPoco进行改写,如增删改查、分页以及存储过程的调用,在文章的最后附上我测试的源码(源码有许多不规范的地方,也有冗余,只是简单地改写后为了快速马上通过函数功能测试,希望大家拍砖指正。)
1、 PetaPoco是C#微型ORM框架,基本无需配置,仅由单个cs文件构成,支持.net3.5 .net4.0。官方还出品了配套的T4模板用来自动生成数据库Models。官方网站:http://www.toptensoftware.com/petapoco/,截稿时PetaPoco的官方最新版本为5.0.1。
2、 从官网下载文件压缩包(另一种安装PetaPoco文件的方式见本文的最后的附录一),下载地址需要注意一下,官方给出的Github下载地址是https://github.com/toptensoftware/PetaPoco,如果你点这个地址下载下来的可能是前一个版本,也就是4.0.3,我个人是遇到这个问题了,以为下载下来的是5.0.1,其实不然,下面给出一个能下载到5.0.1的地址,也是Github上的https://github.com/toptensoftware/PetaPoco/tree/v5,打开链接之后点击右边下载zip包即可。(顺带也提醒一下,官网给出的NuGet下载地址,没有加入版本号,下载到的其实也是4.0.3,所以大家在下载的时候把版本号也加上了,具体请见本文的附录一)。另外,建议大家参与一下官网的这篇文章http://www.toptensoftware.com/Articles/137/Long-Time-No-Post-and-PetaPoco-v5,我也是看完之后才知道我下载的版本不是最新的。说明一下:我下载的时间是2014年4月3日,至于这个日期以后的会不会变化就留给大家去验证了。
3、 先建立一个类库并命名(我的叫做Controller),然后先添加一个App.config文件,创建数据库连接字符串,必须指定为providerName=”System.Data.SqlClient”。把第二步下载到的PetaPoco文件压缩包解压后把T4模板以及PetaPoco.cs文件拷贝到你的项目中,项目结构如下图所示,编辑Database.tt,如果需要自动生成视图的代码,需要加上这一句:IncludeViews = true; 保存文件后再Database.tt 文件上点击右键,运行自定义工具(可能会有一个.net版本警告,可以直接无视),就能在Database.tt下生成一个Database.cs 文件了,文件里面是什么内容,大家打开看一下就明白了。
4、 使用SingleOrDefault或者Delete的时候,参数用object和用string调用的是两个不同的接口,public T SingleOrDefault<T>(object primaryKey)和public T SingleOrDefault<T>(string sql, params object[] args),写法如下:
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
5、 模糊查询,用like关键字的时候需要注意下面的写法,注释掉的那种写法执行不出预期的结果,应该是PetaPoco的一个bug吧:
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
6、 执行存储过程
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
7、 存储过程代码示例:
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
8、 A表(T4模板自动生成对应的A类)Join B表(T4模板自动生成对应的B类),返回结果按平常习惯来说它应该是一个数据集DataSet或者DataTable,但是,PetaPoco没有返回DataSet或者DataTable的,如Fetch<T>返回的是一个List<T>,这时候就会有麻烦了,这个T到底是写A呢还是写B呢?不用纠结,马上告诉你答案:
解决方法1(最为推荐):因为是用T4模板来自动生成数据库表或者视图的模型类,所以不建议在自动生成的代码里面做过多的扩展,否则一重新生成,之前修改的代码就会被覆盖掉了。再来看一下,T4模板生成的是partial class(MSDN给出的官方定义是这样的:分部类型定义允许将类、结构或接口的定义拆分到多个文件中),如下图所示:
接着上面的话题,首先添加一个新类,使用与T4模板生成的类相同的名字(确保名称空间也要匹配),声明为partial class,给任何Join的列添加[ResultColumn]属性,这里我们决定来partial A,把Join后B的属性加进来,代码举例如下图所示,
最后的执行代码是这样的:var result = db.Fetch<A>(sql);
项目结构如下图所示:
解决方法2(这中方法我本人没有试过,待验证):可以自己写一个类C,这个类的属性就是A和B的Join后的所有列,C类示例代码:
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
最后的执行代码:var articles = db.Fetch<C>(sql);
附录一(从NuGet安装PetaPoco,参考来自http://landyer.com/archives/138)
VS2010自带了NuGet,但是版本可能不是最新的(我的就不是)。
首先把NuGet升级到最新版本。
3. 如上图,这种情况就是需要更新《NuGet Package Manager》。如果没有出现,则说明H你的《NuGet》已经是最新的了(前提是你是VS2010),直接进入下一章吧。
4. 点击《更新》,然后按操作进行就ok了。
5. 依次打开《工具》《库程序包管理器》《程序包管理器控制台》
6. 敲入命令对可用的版本进行查询get-package –listavailable –allversion –filter petapoco,如图
7. 我要使用的是petapoco 5.0.1这一个,敲入命令进行安装install-package petapoco –version 5.0.1,如图
中间弹出对话框,直接确定,如果之后报错直接无视就行了。
8. 将当前焦点窗口转移回控制台。如下图输出,表示安装成功。
9. 上图中的《acoms》是我当前的项目,安装的时候默认要有一个打开的项目的Web的桌面的都可以。
10. PetaPoco的安装结束。
附录二(有用的帮助文档)
http://www.toptensoftware.com/petapoco/
http://landyer.com/archives/138
http://www.cnblogs.com/vento/archive/2013/02/09/2909562.html
http://www.cnblogs.com/youring2/archive/2012/06/04/2532130.html
http://www.cnblogs.com/yanxiaodi/archive/2013/03/25/2978606.html
附录三(附上我这边调试好的代码片段)
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
![](https://images.cnblogs.com/OutliningIndicators/ExpandedBlockStart.gif)
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.IO; using System.Runtime.Serialization.Json; namespace Common { /// <summary> /// XML序列化助手类 /// </summary> public class SerializeHelper<T> { public static string Serialize(object obj) { if (obj == null) return null; using (MemoryStream memoryStream = new MemoryStream()) { DataContractJsonSerializer dataContractJsonSerializer = new DataContractJsonSerializer(obj.GetType()); dataContractJsonSerializer.WriteObject(memoryStream, obj); return Encoding.UTF8.GetString(memoryStream.ToArray()); } } public static T Deserialize(string str) { using (MemoryStream memoryStream = new MemoryStream(Encoding.UTF8.GetBytes(str))) { DataContractJsonSerializer dataContractJsonSerializer = new DataContractJsonSerializer(typeof(T)); return (T)dataContractJsonSerializer.ReadObject(memoryStream); } } } }