您的位置:首页 > 移动开发 > Unity3D

Unity C#最佳实践(上)

2016-12-28 21:28 204 查看
本文为《effective c#》的读书笔记,此书类似于大名鼎鼎的《effective c++》,是入门后提高水平的进阶读物,此书提出了50个改进c#代码的原则,但是由于主要针对.net framework,而unity中的mono平台只支持.net framework 2.0,所以有很多原则在unity中并不适用,本文总结了其中在unity中也适用的一些原则。整理后,一共20多个原则仍然适用于unity,将分为两篇文章来记录。

1 使用属性,不使用可访问的数据成员

属性更加灵活,有了新的需求时便于修改。例如定义name属性,后来我们想到名字不应为空,那么只需要修改一处就可以,如果使用公有成员变量,则要在每处使用的地方修改。还可以使用所引器来访问。

public class MyClass
{
private string[] namelist = new string[10];

private string username;

public string Username
{
get { return username; }
set
{
if (string.IsNullOrEmpty(value))
{
throw new ArgumentException("Username can't be blank!");
}
username = value;
}
}

//简单索引器
public string this[int idx]
{
get
{
if (idx < namelist.Length && idx >= 0)
{
return namelist[idx];
}
return string.Empty;
}
set
{
if (idx >= 0 && idx < namelist.Length)
{
namelist[idx] = value;
}
}
}

public void UseIndexor()
{
this[0] = "first name";//效果等同于namelist[0]="first name"
}
}

C#建议总是使用property,但是在Unity中有很多局限性:

1.如果在monobehavior脚本类中使用property,那么在编辑器的inspector中无法显示出property。
2.性能问题,.net4.0以上会将简单的property编译成内敛函数,因此性能影响很小,但是mono不支持,性能会稍差一些。
建议:在类自己中避免使用property,直接使用成员变量;小型的私有类中避免使用;避免在简单的仅仅用来存储数据的类中使用;避免在性能很敏感的部分使用;在monobehavior脚本中,如果需要在编辑器中修改成员,也应该避免使用。除此以外的其他情况,可以使用property。

2 使用运行时常量readonly而不是编译时常量const

const性能有优势但是微乎其微,且只能应用于基本数据类型,readonly更加灵活,可以应用于所有类型。
readonly在构造函数或者初始化器中初始化。
在编译器一定要得到确定的数值时必须使用const,如attribute的参数和枚举的定义等。
其他情况,除非对性能非常敏感,否则通常应该使用readonly。

3 使用as 或 is,不用强制类型转换

使用as后需要检查是否为null ,对于值类型不能使用as,应该使用is。总之,能用as用as,不能则用is判断后用强制类型转换。

public class Test
{
internal class MyType
{
}

internal class MyFactory
{
public static MyType GetObject()
{
return new MyType();
}
}
void asAndIs()
{
object o = MyFactory.GetObject();
MyType t = o as MyType;
if (t != null)
{
//t is a MyType
}
else
{
// failed,throw exception
}

object o2 = MyFactory.GetObject();
//int i = o2 as int;//编译错误
//应该使用is
if (o2 is int)
{
int i2 = (int) o2;
}
else
{
// failed,throw exception
}
}
}

4 使用Conditional attribute而不是#if 条件编译

Conditional应用于方法,因此强制我们把条件代码拆分成不同的方法,这比使用#if结构更加清晰。
使用#if条件编译,当不符合条件时,方法仍然被编译,和调用,只是方法内容为空,而使用Conditional,方法虽然也会被c#编译器编译,但是不会被jit编译器编译,也不会被调用,不会被加载到内存。多个特性默认为or运算,如果有其他需要则需自定义。例子如下:

public class UseConditional
{
[Conditional("DEBUG")]
void NewDebugFunc()
{
//do something
}

void MainFunc()
{
NewDebugFunc();
//do something
}

//多个特性默认为or运算,如果有其他需要则需自定义
[Conditional("DEBUG"), Conditional("REALESE")]
void OrConditionFunc()
{
//如果定义了DEBUG或者REALESE,会执行这个方法
}

//如果要多个条件执行and运算,需要自定义
#if (VAR1 && VAR2)
#define BOTH
#endif

[Conditional("BOTH")]
void AndConditionFunc()
{
//dosomething
}
}

5 等同性判断

4种方法判断等同性
两种静态方法object.Equals(a,b),object.ReferenceEquals(a,b),加上Equals()实例方法和operator==()
object.Equals(a,b)值相等,object.ReferenceEquals(a,b)引用相等(指向相同的实例),这两种静态方法永远不应该去重新定义
对于引用类型来说,这两个方法一样,对于值类型则不同。对于值类型,我们应该总是覆写Equals()实例方法和operator==()
对于引用类型,当我们认为相等的含义并不是对象引用相等时,应该覆写Equals()实例方法,并实现IEquatable<T>接口(为了可以在泛型集合中使用)

注意,覆写Equals()实例方法的同时也要覆写GetHashCode()方法

例子:

//引用类型实现值相等性
class TwoDPoint : IEquatable<TwoDPoint>
{
public int X { get; set; }
public int Y { get; set; }

public TwoDPoint(int x, int y)
{
this.X = x;
this.Y = y;
}

public override bool Equals(object obj)
{
return this.Equals(obj as TwoDPoint);
}

public bool Equals(TwoDPoint other)
{
if (object.ReferenceEquals(other, null))
{
return false;
}
if (object.ReferenceEquals(this, other))
{
return true;
}
if (this.GetType() != other.GetType())
{
return false;
}
return (X == other.X) && (Y == other.Y);
}

public override int GetHashCode()
{
return X*0x00010000 + Y;
}

public static bool operator ==(TwoDPoint lhs, TwoDPoint rhs)
{
if (object.ReferenceEquals(lhs, null))
{
if (object.ReferenceEquals(rhs, null))
{
return true; //null==null = true
}

return false;
}
return lhs.Equals(rhs);
}

public static bool operator !=(TwoDPoint lhs, TwoDPoint rhs)
{
return !(lhs == rhs);
}
}

//值类型实现相等性,默认使用反射方法验证相等性,性能差,所以总是应该自己实现
struct TwoDPointStruct : IEquatable<TwoDPointStruct>
{
public int X { get; set; }
public int Y { get; set; }

public TwoDPointStruct(int x, int y) : this()
{
X = x;
Y = y;
}

public override bool Equals(object obj)
{
if (obj is TwoDPointStruct)
{
return this.Equals((TwoDPointStruct) obj);
}
return false;
}

public bool Equals(TwoDPointStruct other)
{
return (X == other.X) && (Y == other.Y);
}

public override int GetHashCode()
{
return X ^ Y;
}

public static bool operator ==(TwoDPointStruct lhs, TwoDPointStruct rhs)
{
return lhs.Equals(rhs);
}

public static bool operator !=(TwoDPointStruct lhs, TwoDPointStruct rhs)
{
return !(lhs == rhs);
}
}

6 GetHashCode的陷阱

GetHashCode()仅在一个地方用到,为基于散列的集合定义键的散列值时,如HashSet<T>,Dictionary<K,V>容器等。
如果我们定义的类型,不会在容器中作为键使用,那么默认的GetHashCode()方法通常没什么问题,虽然对于引用类型来说性能不高,对于值类型来说(值类型应该是不可变的)也可以正常工作。
对于我们创建的大多数类型来说,最好完全避免实现GetHashCode()
对于值类型,默认的方法会基于第一个字段的GetHashCode()方法,如果这个字段是可变的,则会破坏整体的GetHashCode()方法。
如果要自己实现,通常的做法的所有字段调用GetHashCode再对结果进行^ 异或运算(如果是字段是可变量则排除)。

7 理解短小方法的优势

JIT编译器在运行时把C#编译器生成的IL代码翻译成机器码,这个过程不是在启动一开始就进行的,而是按照函数的粒度来进行,没有被调用的方法不会被JIT编译
因此把逻辑分散在小函数中会比把所有逻辑放在大型函数中更好
如下:第一次调用LargeFunc时,两个分支都将被jit编译

public void LargeFunc(bool flag)
{
if (flag)
{
//do something
}
else
{
//do other things
}
}

//如下函数则只会编译一个分支。另外分支用到时才编译
public void LargeFuncWithShortFunc(bool flag)
{
if (flag)
{
DoSomeThing();
}
else
{
DoOtherThings();
}
}

private void DoOtherThings()
{
//do something
}

private void DoSomeThing()
{
//do other things
}

函数简单短小,也让jit更好的决定哪些变量放到寄存器上,以及哪些函数编译为内联函数。总之,使用简单短小的函数,有助于jit进行更好的优化。

8 推荐使用成员初始化器,而不是赋值语句

随着时间的推移,通常来说构造函数越来愈多,预防这种情况最好的方法就是在变量声明时就进行初始化,而不是在每个构造函数中进行。
初始化器在构造函数前执行,应该充分利用初始化器的语法
private List<string> labels = new List<string>(); //声明同时初始化

有如下三种情况,应该避免使用初始化器
1需要初始化为0或者null的情况,系统默认的初始化工作会在执行所有代码前,把一切设置为0或者null,而且是很底层性能很高的操作自己在设置一次多此一举,下面第二种方法实际上调用了initobj这条IL指令,导致了变量的装箱拆箱,造成了额外的时间消耗。
MyValueType myValue1; //默认0
MyValueType myValue2 = new MyValueType(); //也是0

2当需要对一个变量进行不同的初始化方法时,不应使用初始化器
public P8Init()
{
}

这个构造函数使用不同的方法初始化labels,相当于构造函数之前初始化一次,这里又初始化一次,无谓的消耗应该避免。
public P8Init(int size)
{
labels = new List<string>(size);
}

3最后一种情况是,初始化器无法进行异常处理,当需要进行异常处理时,把初始化工作放到构造函数内。

总上,初始化器是保证成员变量都被初始化的最简单方便的方法,若是所有的构造函数都要将某个成员初始化为同一个值,那么就应该使用初始化器。

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

使用静态初始化器和静态构造函数来初始化静态成员变量
静态初始化器常用于单例模式
private static readonly MyClass instance = new MyClass();

private MyClass ()
{
}

public static MyClass Instance
{
get { return instance; }
}

如果需要更复杂的初始化,或者异常处理,可以使用静态构造函数,静态构造函数在所有实例成员变量初始化器以及实例构造函数前调用。

private static readonly MyClass instance;

private MyClass ()
{
}

public static MyClass Instance
{
get { return instance; }
}

static MyClass ()
{
try
{
instance = new MyClass ();
}
catch (Exception)
{
//handle exception
throw;
}
}

10 尽量减少重复的初始化逻辑

把多个构造函数中的重复逻辑提取到一个公共构造函数中,其他构造函数可以通过构造函数初始化器调用公共构造函数
如下前两个构造函数都调用了第三个构造函数。

private List<object> data;
private string name;

public MyClass():this(0,string.Empty)
{
}

public MyClass(int initCount) : this(initCount, string.Empty)
{
}

public MyClass(int initCount, string name)
{
data = initCount > 0 ? new List<object>(initCount) : new List<object>();
this.name = name;
}


11 避免创建非必要的对象

创建对象会在堆上分配内存,带来GC的工作量,有一些规则可以让你尽量降低GC的工作量,关于Unity中的gc优化,请参考

Unity性能优化(3)-官方教程Optimizing garbage collection in Unity games翻译

未完待续……

内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: