代码改变世界

提高你的C#程序编码质量

2014-04-10 19:53  hduhans  阅读(968)  评论(1编辑  收藏  举报

  摘自陆敏技之《编写高质量代码:改善C#程序的157个建议》,编写C#程序代码时应考虑代码效率、安全和美观,可参考下述建议。想成为一名合格的搬砖工,牢记吧!!

基本语言要素

1、正确操作字符串

   1) 避免装箱操作。如语句:String str = "hans"+8 就存在装箱操作,建议改成语句:String str = "hans"+8.ToString()

   2) 使用StringBuilder代替String运算(经测试,当执行5000次加运算时,StringBuilder效率是String的近600倍)。C#中String一旦被赋值不可改变,进行任何操作(+,=)都会在内存中创建一个新的字符串对象,会给运行计算带来额外开销。而StringBuilder并不会重新创建一个新的String对象,StringBudiler每次执行+操作时,如果内容空间不够(默认长度16),会重新加倍进行分配空间。

//耗时3000毫秒
String str = "";
for (int i = 0; i < 50000; i++) {
    str += i.ToString();
}

//耗时5毫秒
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 50000; i++)
{
    sb.Append(i.ToString());
}
View Code

   3) 常使用String.Format方法。 String.Format在内部使用了StringBuilder方法格式化字符串,效率很高,且代码美观,可读性高。

2、类型转换

   1) 常使用as转换类型。as转换类型效率高,类型转换失败且不会报异常,而是值为null。

   2) 使用TryParse代替Parse。Parse转型失败会引发异常,异常过程会消耗性能,而TryParse转型失败无异常,out操作符将参数设置为0。经测试,当转型失败时,TryParse效率要比Parse高几百倍

   3) 使用int?使得值类型也可以为nul。T?是Nullable<T>的简写,值可以为nul。T?判断是否为nul可用简写操作符??,如int? i=22;int j = i ?? 0。

3、区别readonlyconst使用方法。const是一个编译期常量,readonly是一个运行时常量。const在编译时,会将常量用对应的值替代,运行效率高,而readonly在运行时初始化,初始化后不可修改,运行效率比const低,但是灵活性高。readonly赋值发生在运行时,赋值后不可改变表示:1) 值类型,只本身不可改变 2) 引用类型,引用指针不可改变,即不可修改指针指向新对象,但对象内容可修改

4、避免给enum枚举类型的元素提供显示的值。在如下枚举类型Week中增加一个元素,输出ValueTemp的值等于Wednesday,原因是ValueTemp定义时没有赋值,编译期会逐个为元素值+1,当编译器发现ValueTemp时,会在Tuesday = 2的基础上+1,所以ValueTemp实际赋值为3,与Wednesday=3相等。

enum Week { Monday = 1, Tuesday = 2, ValueTemp, Wednesday = 3, Thursday = 4, Friday = 5, Saturday = 6, Sunday = 7 }

5、熟悉运算符重载。运算符重载可使得对象运算操作简洁方便,参考下例:

class Salary
{
    public int RMB { get; set; }

    //运算符重载
    public static Salary operator +(Salary s1, Salary s2) {
        s2.RMB += s1.RMB;
        return s2;
    }
}

//调用
Salary s1 = new Salary() { RMB = 3 };
Salary s2 = new Salary() { RMB = 5 };
Salary s3 = s1 + s2;   //运用运算符重载
View Code

6、实现深拷贝浅拷贝。浅拷贝是将对象中的所有字段复制到新的对象中,其中,值类型字段拷贝后副本的修改不会影响源对象对应值,而引用字段拷贝后与原字段指向同一对象地址,副本值修改后会影响源对象对应的值(参考《C#值类型与引用类型区别》);深拷贝是将对象中的所有值类型和引用类型字段复制到新对象中,引用类型字段重新创建引用对象,副本的修改不影响源对象对应值。

//浅拷贝 继承ICloneable接口并实现Clone方法
class Salary : ICloneable
{
    public int RMB { get; set; }

    public object Clone()
    {
        //实现浅拷贝
        return this.MemberwiseClone(); 
    }
}

//深拷贝,通过对象序列化和反序列化实现,继承接口ICloneable并实现方法Clone
[Serializable]
class Salary : ICloneable
{
    public int RMB { get; set; }

    public object Clone()
    {
        using (Stream objectStream = new MemoryStream())
        {
            IFormatter formatter = new BinaryFormatter();
            formatter.Serialize(objectStream, this);
            objectStream.Seek(0, SeekOrigin.Begin);
            return formatter.Deserialize(objectStream) as Salary;
        }
    }
}
View Code

7、使用dynamic简化反射操作。 dynamic是Framework 4.0的新特性,可以使C#具有弱语言的特性。

   1) var与dynamic的区别:var在编译的时候替换成自动匹配的实际类型,而dynamic被编译后实际上是一个Object类型,只是编译期会进行特殊处理,在编译器不进行任何的类型检查,而是将类型检查放到了运行期。 

   2) 反射的优化,参考下例:

class A
{
    public String Name { get; set; }

    public int Add(int a, int b)
    {
        return a + b;
    }
}

//调用
A a1 = new A();
MethodInfo m = a1.GetType().GetMethod("Add");

//普通反射 耗时1084ms
for (int i = 0; i < 1000000; i++)
{
    int re = (int)m.Invoke(a1, new object[] { 3, 4 });
}

//优化后的反射 耗时13ms
var delg = (Func<A, int, int, int>)Delegate.CreateDelegate(typeof(Func<A, int, int, int>), m);
for (int i = 0; i < 1000000; i++)
{
    delg(a1, 3, 4);
}

//使用dynamic优化反射 耗时60ms
dynamic a2 = new A();
for (int i = 0; i < 1000000; i++) {
    a2.Add(3, 4);
}

//使用dynamic优化反射 耗时60ms
dynamic a2 = new A();
for (int i = 0; i < 1000000; i++) {
     a2.Add(3, 4);
}
View Code

8、使用Environment.NewLine获取当前环境下的换行符号。

9、使用params减少重复参数。如方法:public void pap(String a,String b,String c){ } 可简写为:public void pap(params String[] args){ }。注意:① params数组必须是方法的最后一个参数 ② 不允许out或ref数组

10、扩展类型中的方法。扩展方法是一种特殊的静态方法,可以为类型扩展方法而无需创建新的派生类型。本实例扩展了String类型添加了扩展方法Test():

//自定义扩展类 必须为静态类
static class StringExtenstion
{
    //扩展String类添加方法Test,必须为静态方法,参数格式为:this 类型名称 对象
    //调用方法如:String str="hans"; Console.WriteLine(str.Test());  --输出my string
    public static String Test(this String str) {
        return "my string";
    }
}
View Code
集合和LINQ

1、对象和集合初始化:Person person = new Person(){ Name="hans",Age = 25 };

2、匿名类型:var persion = new { Name="hans",Age=25 };  编译器会自动生成具有对应字段的匿名类。

3、LINQ查询中避免不必要的迭代。充分运用First和Take等方法,查询到符合条件的记录就立即返回,而不是所有结果返回再筛选,效率可大幅度提高。

资源管理和序列化

1、继承IDispose接口的类型,实例化可用using语法。using会在结束时,自动调用对象的Dispose方法。

2、通用BinarySerializer序列化。BinarySerializer.cs:

class BinarySerializer
{

    //将类型序列化为字符串
    public static string Serialize<T>(T t)
    {
        using (MemoryStream stream = new MemoryStream()) {
            BinaryFormatter formatter = new BinaryFormatter();
            formatter.Serialize(stream, t);
            return System.Text.Encoding.UTF8.GetString(stream.ToArray());
        }
    }

    //将类型序列化为文件
    public static void SerializeToFile<T>(T t, string path, string fullName)
    {
        if (!Directory.Exists(path)) {
            Directory.CreateDirectory(path);
        }
        string fullPath = string.Format(@"{0}\{1}", path, fullName);
        using (FileStream stream = new FileStream(fullPath, FileMode.OpenOrCreate)) {
            BinaryFormatter formatter = new BinaryFormatter();
            formatter.Serialize(stream, t);
            stream.Flush();
        }
    }

    //将字符串反序列化为类型
    public static TResult Deserialize<TResult>(string s) where TResult : class
    {
        byte[] bs = System.Text.Encoding.UTF8.GetBytes(s);
        using (MemoryStream stream = new MemoryStream(bs)) {
            BinaryFormatter formatter = new BinaryFormatter();
            return formatter.Deserialize(stream) as TResult;
        }
    }

    //将文件反序列化为类型
    public static TResult DeserializeFromFile<TResult>(string path) where TResult : class
    {
        using (FileStream stream = new FileStream(path, FileMode.Open)) {
            BinaryFormatter formatter = new BinaryFormatter();
            return formatter.Deserialize(stream) as TResult;
        }
    }
}
View Code

3、序列化特性说明: 

   1) Serializable:用于类,指示一个类可以序列化;

   2) NonSerialized:用于字段,指示一个字段不被序列化。因为属性的本质是方法,因此NonSerialized不可直接用于属性,可用于自己实现的属性;      

   3) OnDeserialized:应用于某方法时,会指定在对象反序列化后立即调用此方法;

   4) OnDeserializing:应用于某方法时,会指定在对象反序列化时调用此方法;

   5) OnSerialized:如果将对象图应用于某方法,则应指定在序列化该对象图后是否调用该方法;

   6) OnSerializing:当他应用于某个方法时,会指定在对象序列化前调用此方法;

异步、多线程、任务和并行

1、异步与多线程。异步与多线程两者度可以达到避免调用线程阻塞的目的,从而提高软件的可响应性。很多时候,我们分不清异步与多线程的区别,经常经他们混为一谈,其实,他们还是有区别的:

   1) 异步操作本质:所有的程序最终都会由计算机硬件来执行,拥有DMA功能的硬件在和内存进行数据交互的时候可以不消耗CPU资源,这些不消耗CPU时间的I/O操作正是异步操作的硬件基础。所以即使在DOS这样的单进程系统中也同样可以发起异步的DMA操作。优点异步操作无需额外线程负担,并且使用了回调的方式进行处理,在设计良好的情况下,处理函数可尽可能减少共享变量的使用,减少了死锁发生的可能性缺点异步操作编写复杂,回调难以调试。

   2) 线程本质:线程不是一个计算机硬件的功能,而是操作系统提供的一种逻辑功能。线程本质上是进程总一段并发执行的代码,所以线程需要操作系统投入CPU资源来运行和调度。优点编写简单缺点线程使用会消耗额外切换带来的负担,并且线程间共享变量可能造成死锁。

   3) 适用范围:多线程适用于计算密集型工作,异步机制适用于IO密集型工作,详细参考图1。

图1 单线程、多线程适用条件

   一个使用了异步操作的WinForm程序示例如下所示,点击按钮,异步获取网页源码并显示在窗体的文本控件textBox1上。

private void button1_Click(object sender, EventArgs e)
{
    //开辟一个线程
    Thread t = new Thread(()=>{
        var request = HttpWebRequest.Create("http://www.cnblogs.com/hanganglin");
        //发起异步请求
        request.BeginGetResponse(this.AsyncCallbackImpl, request);
    });
    t.Start();
}

//回调方法
public void AsyncCallbackImpl(IAsyncResult ar) {
    WebRequest request = ar.AsyncState as WebRequest;
    var response = request.EndGetResponse(ar);
    var stream = response.GetResponseStream();
    using (StreamReader reader = new StreamReader(stream)) {
        var content = reader.ReadToEnd();
        //由于textBox1控件是主线程创建的,在其他线程中需要调用必须采用异步机制
        //如果InvokeRequired为True,则必须通过异步来修改,否则可直接修改
        if (textBox1.InvokeRequired) {
            textBox1.BeginInvoke(new Action(() => {
                textBox1.Text = content;
            }));
        }
        else {
            textBox1.Text = content;
        }
    }
}
View Code

   值得注意的是,创建控件线程以外的线程想访问控件,可通过控件的BeginInvoke异步方法,BeginInvoke方法是将消息发送到消息队列中等待UI所在的线程进行处理,代码:if(textBox1.InvokeRequired){ textBox.BeginInvoke(new Action(()=>{ textBox.Text = content; })); } else { textBox1.Text = content; }

安全性设计

1、声明变量时考虑最大值,关键字check可检查运算是否溢出,运算溢出则抛出异常。 代码:check{ ... }。

2、文件MD5哈希值判断文件内容是否修改。对文件求MD5哈希值,当文件内容被修改后再求MD5哈希值,比较两个值可判断文件内容是否被修改过。

//获取文件的md5哈希值
public static String GetFileMd5Hash(String filePath) { 
    using(MD5CryptoServiceProvider md5 = new MD5CryptoServiceProvider())
    using(FileStream fs = new FileStream(filePath,FileMode.Open,FileAccess.Read,FileShare.Read)){
        return BitConverter.ToString(md5.ComputeHash(fs)).Replace("-", "");
    }
}
View Code

3、合适选择使用对称加密非对称加密。对称加密加密和解密时使用了相同的密钥和加密算法,其优点是加密解密速度快,常用于大量数据传输,缺点是传输数据时需要传输密钥, 安全系数不高。非对称加密使用了不同的密钥,公钥PK和私钥SK,用公钥PK进行加密,只有用对应的私钥SK才可以解密,优点是传输加密信息时不需要传输私钥,安全系数高,缺点是算法复杂,加密解密速度很慢。

   C#下的一个文件对称加密示例MySymmetricAlgorithm: 

public class MySymmetricAlgorithm
{
//缓冲区大小
static int bufferSize = 128 * 1024;
//密钥salt  防止“字典攻击”
static byte[] salt = { 134, 216, 7, 36, 88, 164, 91, 227, 174, 76, 191, 197, 192, 154, 200, 248 };
//初始化向量
static byte[] iv = { 134, 216, 7, 36, 88, 164, 91, 227, 174, 76, 191, 197, 192, 154, 200, 248 };

//初始化并返回对称加密算法
static SymmetricAlgorithm CreateRijndael(string password, byte[] salt)
{
    PasswordDeriveBytes pdb = new PasswordDeriveBytes(password, salt, "SHA256", 1000);
    SymmetricAlgorithm sma = Rijndael.Create();
    sma.KeySize = 256;
    sma.Key = pdb.GetBytes(32);
    sma.Padding = PaddingMode.PKCS7;
    return sma;
}

public static void EncryptFile(string inFile, string outFile, string password)
{
    using (FileStream inFileStream = File.OpenRead(inFile), outFileStream = File.Open(outFile, FileMode.OpenOrCreate))
    using (SymmetricAlgorithm algorithm = CreateRijndael(password, salt)) {
        algorithm.IV = iv;
        using (CryptoStream cryptoStream = new CryptoStream(outFileStream, algorithm.CreateEncryptor(), CryptoStreamMode.Write)) {
            byte[] bytes = new byte[bufferSize];
            int readSize = -1;
            while ((readSize = inFileStream.Read(bytes, 0, bytes.Length)) != 0) {
                cryptoStream.Write(bytes, 0, readSize);
            }
            cryptoStream.Flush();
        }
    }
}

public static void DecryptFile(string inFile, string outFile, string password)
{
    using (FileStream inFileStream = File.OpenRead(inFile), outFileStream = File.OpenWrite(outFile))
    using (SymmetricAlgorithm algorithm = CreateRijndael(password, salt)) {
        algorithm.IV = iv;
        using (CryptoStream cryptoStream = new CryptoStream(inFileStream, algorithm.CreateDecryptor(), CryptoStreamMode.Read)) {
            byte[] bytes = new byte[bufferSize];
            int readSize = -1;
            int numReads = (int)(inFileStream.Length / bufferSize);
            int slack = (int)(inFileStream.Length % bufferSize);
            for (int i = 0; i < numReads; ++i) {
                readSize = cryptoStream.Read(bytes, 0, bytes.Length);
                outFileStream.Write(bytes, 0, readSize);
            }
            if (slack > 0) {
                readSize = cryptoStream.Read(bytes, 0, (int)slack);
                outFileStream.Write(bytes, 0, readSize);
            }
            outFileStream.Flush();
        }
    }
}
View Code
类型设计

1、区分接口抽象类的应用场合。接口与抽象类的区别:① 接口支持多继承,抽象类只能但继承; ② 接口可以包含方法、属性、索引器、事件的签名,但不能有实现,抽象类则可以通过虚方法来实现; ③ 接口新增方法后,所有继承者必须重构,否则编译不通过,而抽象类新增虚方法后不需要(新增抽象方法也需重构)。由于存在这些区别,接口一旦被设计出来,就应该是不变的,而抽象类可以随着版本的升级增加一些功能。接口与抽象类的应用场景简单可概括为:① 如果对象存在若干功能相近且关系紧密的版本,则使用抽象类; ② 如果对象关系不紧密,但是若干功能拥有共同的声明,则使用接口; ③ 抽象类适合于提供丰富功能的场合,接口则更倾向于提供单一的一组功能

2、优先考虑组合(Has a),然后考虑继承(Is a)。组合是将其他类型的对象作为本类型的成员使用,而继承是子类继承父类并使用。组合好比"黑盒式代码使用",继承好比"白盒式代码使用"。组合的耦合性比继承更低,封装性比继承更高。

3、开闭原则。开闭原则是面向对象设计中最重要的原则之一,是可复用设计的基石。开闭原则原话翻译:软件实体应该对扩展开放,对修改关闭。通俗地说,在软件体系扩展新功能时,不应该修改现有的代码

命名规范

1、命名术语:

   1) PascalCasing帕斯卡命名法(首字母大写),公开元素建议使用帕斯卡命名法。建议用于命名空间、类型、接口、方法、属性、事件、静态字段和枚举值。

   2) camelCasing驼峰命名法(首字母小写),非公开元素建议使用驼峰命名法。建议用于参数、私有字段和方法内变量。

2、命名规范:

   1) 命名空间:使用Java中的域名域名命名法,或使用公司名作为前缀,产品名称作为第二层,其他特性作为第三层,如:PanChina.Oa.System。

   2) 类型:使用名词或名词词组进行命名(如UserManager要优于UserManage),不要在类型名前加前缀,派生类名称以基类名称结尾(如Exception所有派生类都以Exception结尾)。

   3) 接口:使用大写字母"I"为前缀,用形容词命名,如IDisposable表示类型可以被释放。

   4) 泛型:使用大写字母"T"为前缀,多个参数使用标号,如T1、T2。

   5) 枚举:枚举类型用复数命名,不要添加如"Enum"或"Flag"等后缀,枚举元素用单数命名。如enum Week { Monday,Tuesdat,.. }

   6) 字段:共有字段使用帕斯卡命名法,私有字段使用驼峰命名法。使用名词或名词词组命名,不添加前缀。

   7) 方法:使用动词词组命名,根据方法对应的任务命名,而不是根据内部实现细节来命名。常用动词:Get、Update、Delete、Add、Validate、Select、Search等,动词后加上动作内容,就是一个规范的方法名。

   8) 属性:用名词或名词词组命名,要用肯定性的短语,如CanSeek,而不是否定短语CantSeek。当属性对应一个类型时,建议则接用类型命名属性名,如:public Company Company{ get;set; },不建议为属性制定另外名字,如TheCompany。

   9) 事件:用动词或动词词组命名(如Cheked、Updated、Selected等词组),以"EventArgs"后缀结尾(绑定事件的方法名加上On)。如:UpdatedEventArgs,绑定事件方法OnUpdated()。

   10) 索引器:固定设计,使用this关键字,如:public String this[int index] { get {return "";} }。

3、有条件地使用前缀,在.NET设计规范中,不建议使用前缀,如果确有特殊使用需求,建议:① 前缀m_,表示这是一个实例变量 ② 前缀s_,表示这是一个静态变量。