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


      摘自陆敏技之《编写高质量代码:改善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_,表示这是一个静态变量。

  • 相关阅读:
    SpringBoot(十二)------国际化配置
    SpringBoot(十一) ----SpringBoot结合mybatis实现增删改查
    SpringBoot(十)----SpringMVC自动配置&扩展配置
    SpringBoot学习(九) ------访问静态web资源
    leetcode-----两数相加
    JDBC — 学习大纲
    网络编程
    StringBuffer
    代理
    加载文件的两种方式
  • 原文地址:https://www.cnblogs.com/hanganglin/p/3657260.html
Copyright © 2020-2023  润新知