提高Unity中C#代码质量的21条准则


1、尽可能使用属性,而不是直接访问数据成员

        get set方法


2、偏向于使用运行时常量而不是编译时常量

        编译时常量(const)的值会被目标代码中的值直接取代

            仅能用于数值和字符串

        运行时常量(readonly)的值是在运行是进行求值

            可以为任意类型


3、推荐使用is 或as操作符而不是强制类型转换

         is : 检查一个对象是否兼容于其他指定的类型,并返回一个Bool值,永远不会抛出异常。

        as:作用与强制类型转换是一样,但是永远不会抛出异常,即如果转换不成功,会返回null。

            as运算符对值类型是无效,此时可以使用is,配合强制类型转换进行转换。


4,推荐使用条件属性而不是#if条件编译

        由于#if/#endif很容易被滥用,使得编写的代码难于理解且更难于调试。C#为此提供了一条件特性(Conditional attribute)。使用条件特性可以将函数拆分出来,让其只有在定义了某些环境变量或设置了某个值之后才能编译并成为类的一部分。Conditional特性最常用的地方就是将一段代码变成调试语句。


5、理解几个等同性判断之间的关系

        C#中可以创建两种类型:值类型和引用类型。如果两个引用类型的变量指向的是同一个对象,它们将被认为是“引用相等”。如果两个值类型的变量类型相同,而且包含同样的内容,它们被认为是“值相等”。这也是等同性判断需要如此多方法的原因。

        当我们创建自己的类型时(无论是类还是struct),应为类型定义“等同性”的含义。C#提供了4种不同的函数来判断两个对象是否“相等”。

        public static bool ReferenceEquals (object left, object right);判断两个不同变量的对象标识(object identity)是否相等。无论比较的是引用类型还是值类型,该方法判断的依据都是对象标识,而不是对象内容。

        public static bool Equals (object left, object right); 用于判断两个变量的运行时类型是否相等。

        public virtual bool Equals(object right); 用于重载

        public static bool operator ==(MyClass left, MyClass right); 用于重载


6、了解GetHashCode()的一些坑

        GetHashCode()函数仅会在一个地方用到,即为基于散列(hash)的集合定义键的散列值时,此类集合包括HashSet和Dictionary容器等。对引用类型来讲,索然可以正常工作,但是效率很低。对值类型来讲,基类中的实现有时甚至不正确。而且,编写的自己GetHashCode( )也不可能既有效率又正确。


7、理解短小方法的优势

        将C#代码翻译成可执行的机器码需要两个步骤。C#编译器将生成IL,并放在程序集中。随后,JIT将根据需要逐一为方法(或是一组方法,如果涉及内联)生成机器码。短小的方法让JIT编译器能够更好地平摊编译的代价。短小的方法也更适合内联。

        除了短小之外,简化控制流程也很重要。控制分支越少,JIT编译器也会越容易地找到最适合放在寄存器中的变量。

        所以,短小方法的优势,并不仅体现在代码的可读性上,还关系到程序运行时的效率。


8、选择变量初始化而不是赋值语句

        成员初始化器是保证类型中成员均被初始化的最简单的方法——无论调用的是哪一个构造函数。初始化器将在所有构造函数执行之前执行。使用这种语法也就保证了你不会再添加的新的构造函数时遗漏掉重要的初始化代码。

        综上,若是所有的构造函数都要将某个成员变量初始化成同一个值,那么应该使用初始化器。


9、正确地初始化静态成员变量

        C#提供了有静态初始化器和静态构造函数来专门用于静态成员变量的初始化。

        静态构造函数是一个特殊的函数,将在其他所有方法执行之前以及变量或属性被第一次访问之前执行。可以用这个函数来初始化静态变量,实现单例模式或执行类可用之前必须进行的任何操作。


10、使用构造函数链(减少重复的初始化逻辑)

        多个构造函数包含相同的逻辑,可以将这个逻辑提取到一个通用的构造函数中。

        构造函数初始化器允许一个构造函数去调用另一个构造函数。


11、实现标准的销毁模式

        GC可以高效地管理应用程序使用的内存。不过创建和销毁堆上的对象仍旧需要时间。若是在某个方法中创建了太多的引用对象,将会对程序的性能产生严重的影响。

        这里有一些规则,可以帮你尽量降低GC的工作量:

            若某个引用类型(值类型无所谓)的局部变量用于被频繁调用的例程中,那么应该将其提升为成员变量。

            为常用的类型实例提供静态对象。

            创建不可变类型的最终值。比如string类的+=操作符会创建一个新的字符串对象并返回,多次使用会产生大量垃圾,不推荐使用。对于简单的字符串操作,推荐使用string.Format。对于复杂的字符串操作,推荐使用StringBuilder类。

12、区分值类型和引用类型

        C#中,class对应引用类型,struct对应值类型。

        值类型无法实现多态,因此其最佳用途就是存放数据。引用类型支持多态,因此用来定义应用程序的行为。

        一般情况下,我们习惯用class,随意创建的大都是引用类型,若下面几点都肯定,那么应该创建struct值类型:

            该类型主要职责在于数据存储吗?

            该类型的公有接口都是由访问其数据成员的属性定义的吗?

            你确定该类型绝不会有派生类型吗?

            你确定该类型永远都不需要多态支持吗?

        用值类型表示底层存储数据的类型,用引用类型来封装程序的行为。这样,你可以保证类暴露出的数据能以复制的形式安全提供,也能得到基于栈存储和使用内联方式存储带来的内存性能提升,更可以使用标准的面向对象技术来表达应用程序的逻辑。而倘若你对类型未来的用图不确定,那么应该选择引用类型。

13、保证0为值类型的有效状态

        在创建自定义枚举值时,请确保0是一个有效的选项。若你定义的是标志(flag),那么可以将0定义为没有选中任何状态的标志(比如None)。即作为标记使用的枚举值(即添加了Flags特性)应该总是将None设置为0。


14、保证值类型的常量性和原子性

        常量性的类型使得我们的代码更加易于维护。不要盲目地为类型中的每一个属性都创建get和set访问器。对于那些目的是存储数据的类型,应该尽可能地保证其常量性和原子性。

            对象的常量性:对象的状态一旦确定,就不能再次更改了。如果想再次更改,需要重新构造一个对象。

            对象的原子性:对象的状态是一个整体,如果一个字段改变,其他字段也要同时做出相应改变。简单来说,就是要么不改,要么全改。

            1、当创建类型的目的是为了存储一组相关的数据,且数据量不是很大的时候,将它声明为Struct比Class会获得更高的效率;

            2、将类型声明为具有原子性和常量性,可以避免可能出现的数据不一致问题;

            3、通过在构造函数和Get访问器中,对对象的字段进行深度复制,可以避免在类型的外部修改类型内部数据的问题。


15、限制类型的可见性

        在保证类型可以完成其工作的前提下。你应该尽可能地给类型分配最小的可见性。也就是,仅仅暴露那些需要暴露的。尽量使用较低可见性的类来实现公有接口。可见性越低,能访问你功能的代码越少,以后可能出现的修改也就越少。


16、通过定义并实现接口替代继承

        理解抽象基类(abstract class)和接口(interface)的区别:

            接口是一种契约式的设计方式,一个实现某个接口的类型,必须实现接口中约定的方法。抽象基类则为一组相关的类型提供了一个共同的抽象。也就是说抽象基类描述了对象是什么,而接口描述了对象将如何表现其行为。

            接口不能包含实现,也不能包含任何具体的数据成员。而抽象基类可以为派生类提供一些具体的实现。

            基类描述并实现了一组相关类型间共用的行为。接口则定义了一组具有原子性的功能,供其他不相关的具体类型来实现。

        理解好两者之间的差别,我们便可以创造更富表现力、更能应对变化的设计。使用类层次来定义相关的类型。用接口暴露功能,并让不同的类型实现这些接口。


17、理解接口方法和虚方法的区别

        第一眼看来,实现接口和覆写虚方法似乎没有什么区别,实际上,实现接口和覆写虚方法之间的差别很大。

        接口中声明的成员方法默认情况下并非虚方法,所以,派生类不能覆写基类中实现的非虚接口成员。若要覆写的话,将接口方法声明为virtual即可。

        基类可以为接口中的方法提供默认的实现,随后,派生类也可以声明其实现了该接口,并从基类中继承该实现。

        实现接口拥有的选择要比创建和覆写虚方法多。我们可以为类层次创建密封(sealed)的实现,虚实现或者抽象的契约。还可以创建密封的实现,并在实现接口的方法中提供虚方法进行调用。


18、用委托实现回调

        在C#中,回调是用委托来实现的,主要要点如下:

            委托为我们提供了类型安全的回调定义。虽然大多数常见的委托应用都和事件有关,但这并不是C#委托应用的全部场合。当类之间有通信的需要,并且我们期望一种比接口所提供的更为松散的耦合机制时,委托便是最佳的选择。

            委托允许我们在运行时配置目标并通知多个客户对象。委托对象中包含一个方法的应用,该方法可以是静态方法,也可以是实例方法。也就是说,使用委托,我们可以和一个或多个在运行时联系起来的客户对象进行通信。

            由于回调和委托在C#中非常常用,以至于C#特地以lambda表达式的形式为其提供了精简语法。

            由于一些历史原因,.NET中的委托都是多播委托(multicast delegate)。多播委托调用过程中,每个目标会被依次调用。委托对象本身不会捕捉任何异常。因此,任何目标抛出的异常都会结束委托链的调用。


19、用事件模式实现通知

        事件提供了一种标准的机制来通知监听者,而C#中的事件其实就是观察者模式的一个语法上的快捷实现。

        事件是一种内建的委托,用来为事件处理函数提供类型安全的方法签名。任意数量的客户对象都可以将自己的处理函数注册到事件上,然后处理这些事件,这些客户对象无需在编译器就给出,事件也不必非要有订阅者才能正常工作。

        在C#中使用事件可以降低发送者和可能的通知接受者之间的耦合,发送者可以完全独立于接受者进行开发。


20、避免返回对内部类对象的引用

        若将引用类型通过公有接口暴露给外界,那么对象的使用者即可绕过我们定义的方法和属性来更改对象的内部结构,这会导致常见的错误。

        共有四种不同的策略可以防止类型内部的数据结构遭到有意或无意的修改:

            1,值类型。当客户代码通过属性来访问值类型成员时,实际返回的是值类型的对象副本。

            2,常量类型。如System.String。

            3,定义接口。将客户对内部数据成员的访问限制在一部分功能中。

            4,包装器(wrapper)。提供一个包装器,仅暴露该包装器,从而限制对其中对象的访问。


21、仅用new修饰符处理基类更新

        使用new操作符修饰类成员可以重新定义继承自基类的非虚成员。

        new修饰符只是用来解决升级基类所造成的基类方法和派生类方法冲突的问题。

        new操作符必须小心使用。若随心所欲的滥用,会造成对象调用方法的二义性。

你可能感兴趣的:(提高Unity中C#代码质量的21条准则)