就是把一个需求/问题,分成一步步的逻辑,很像数学里的解答。
关心的是解决问题的步骤。
把需求分为一个个对象。
关心的是对象在做什么。以及对象之间的交互。
设计角度:现有对象,再有类。根据需求,我们从中分析,提取出一有相同行为的"类”。
编码角度:肯定是先有类了,不然new 什么呢?
类是抽象的。是抽象的"类别"。类与类行为不同。
什么是行为?我的理解其实就是代码,如果一个类的代码改变了,那么它的逻辑也改变,逻辑改变体现在对象的实际执行上,也就是对象的行为也会因为逻辑的改变而改变。
那么不同类的代码肯定是不一样的,所以行为也不一样。
对象和对象数据不同。
不同类的对象就不说了。同类型的对象呢?
比如人这个类。
游戏的捏脸功能就是如此,修改一些不同的属性数据即可。比如有的人腿长,有的人腿短,只是数据不同。
但是如果需求要人能飞,能射出蛛丝,“一般"来说我们就得新建一个类了。给它加上 飞行/喷射蛛丝 的行为。
所以,对象和对象的不同,只是数据上,比如腿的长短。而类就得是 行为/功能 上的不同。
分而治之
将一个大的需求分解为许多类,每个类处理一个独立的模块。
拆分好处:独立模块便于分工,每个模块便于复用,可扩展性强。
封装变化
变化的地方独立封装,避免影响其他模块。
高内聚
类中各个方法都在完成一项任务(单一职责的类)。
复杂的实现封装在内部,对外提供简单的调用。
低耦合
类与类的关联性依赖度要低(每个类独立)。
让一个模块的改变,尽少影响其他模块。
分而治之其实就是模块化。
封装变化:
比如我们可以攻击,但是攻击可以分为用手,用武器,等。那么我们就应该用一个专门的武器类来封装这些。
难点在于,什么时候该用类来封装。老师的说法是,变化。
就比如攻击,当攻击方式多样化的时候,就应该封装了。
也很简单。老师也列了一个例子。
[例如:硬件高度集成化,又要可插拔]
最高的内聚莫过于类中仅包含1个方法,将会导致高内聚高耦合。
最低的耦合莫过于类中包含所有方法,将会导致低耦合低内聚。
高内聚高耦合
说的是,如果类都是最简单的一个方法那种,是高内聚了,但是实现一个功能,往往不可能这么简单,那么就得调用多个类,导致高耦合。
低耦合低内聚
极端的一个类囊括所有功能。低耦合的确,但是各种功能都在一个类,导致了低内聚。
复习一些基础知识
深入理解堆栈、堆在内存中的实现
什么是堆?什么是栈?他们之间有什么区别和联系?
推荐看这篇知乎回答。
管理方式:栈由编译器自动管理,无需人为控制。而堆释放工作由程序员控制,容易产生内存泄漏(memory leak)。
空间大小:在32位系统下,堆内存可以达到4G的空间(虚拟内存的大小,有面试官问过),从这个角度来看堆内存大小可以很大。但对于栈来说,一般都是有一定的空间大小的。
碎片问题:堆频繁new/delete会造成内存空间的不连续,造成大量的碎片,使程序效率降低(如何解决?如内存池、伙伴系统等)。对栈来说不会存在这个问题,因为栈是先进后出,不可能有一个内存块从栈中间弹出。在该块弹出之前,在它上面的(后进的栈内容)已经被弹出。
生长方向:堆生长(扩展)方向是向上的,也就是向着内存地址增加的方向;栈生长(扩展)方向是向下的,是向着内存地址减小的方向增长, 可以看第一张图。
分配方式:堆都是动态分配的,没有静态分配的堆。而栈有2种分配方式:静态分配和动态分配。静态分配是编译器完成的,如局部变量分配。动态分配由alloca函数进行分配,但是栈的动态分配和堆是不同的,它的动态分配是由编译器进行释放,无需我们手工实现。
效率:栈是机器系统提供的数据结构,计算机会在底层对栈提供支持(有专门的寄存器存放栈的地址,压栈出栈都有专门的机器指令执行),这就决定了栈的效率比较高。堆则是C/C++函数库提供的,它的机制是很复杂的(可以了解侯捷老师的内存管理的视频,关于malloc/realloc/free函数等)。例如分配一块内存,堆会按照一定的算法,在堆内存中搜索可用的足够大小的空间,如果没有(可能是由于内存碎片太多),就有可能调用系统功能去增加程序数据段的内存空间,这样就有机会分到足够大小的内存,然后进行返回。总之,堆的效率比栈要低得多。
以上来自链接的文章中
这是老师列出来的图。左侧是栈,右侧是堆。
只表示做什么,有什么数据,不做具体处理。
原来是每个员工都需要计算薪资,所以基类是Employee。
后来有需求,可以改变一个员工的职位。
所以,Employee单独作为“员工”独立出来,而每个职位“Programer”,“Tester”,作为员工的职位,继承于基类Job。
泛化:
子类与父类的关系,概念的复用,耦合度最高。因为父类以上,只要改变,子类也全会变。
B类泛化A类,意味B类是A类的一种;
做法:B类继承A类
实现:
抽象行为的具体实现,两者体现功能的关系,变化只影响行为;
A类实现B类,意味A类必须具体实现B类中所有抽象成员。
做法:实现抽象类、接口中的抽象成员。
关联(聚合/组合):
部分与整体的关系,功能的复用,变化影响一个类;
A与B关联,意味着B是A的一部分;
做法:在A类中包含B类型成员。
依赖:
合作关系,一种相对松散的协作,变化影响一个方法;
A类依赖B类,意味A类的某些功能靠B类实现;
做法:B类型作为A类中方法的参数,并不是A的成员。
如何判断两种类的耦合度,从上到下依次减少
开闭原则(目标、总的指导思想)
Open Closed Principle
对扩展开放,对修改关闭。
增加新功能,不改变原有代码。
类的单一职责(一个类的定义)
Single Responsibility Principle
一个类有且只有一个改变它的原因。
适用于基础类,不适用基于基础类构建复杂的聚合类。
依赖倒置(依赖抽象)
Dependency Inversion Principle
客户端代码(调用的类)尽量依赖(使用)抽象的组件。
抽象的是稳定的。实现是多变的。
比如出行,出现是抽象的,但是实际上可以开车,走路等等
组合复用原则(复用的最佳实践)
Composite Reuse Principle
如果仅仅为了代码复用优先选择组合复用,而非继承复用。
组合的耦合性相对继承低。耦合低了,自然以后修改,增加也更容易了。
里氏替换(继承后的重写,指导继承的设计)
Liskov Substitution Principle
父类出现的地方可以被子类替换,在替换后依然保持原功能。
子类要拥有父类的所有功能。
子类在重写父类方法时,尽量选择扩展重写,防止改变了功能。
// 调用 base.xxx 就是所谓的 “拓展“重写
// 尽量,不是一定
public void xxx(){
base.xxx();
....
}
接口隔离(功能拆分)
Interface Segregation Principle
尽量定义小而精的接口interface,少定义大而全的接口。本质与单一职责相同。
小接口之间功能隔离,实现类需要多个功能时可以选择多实现.或接口之间做继承。
例子:IPointerClickHandler
IPointerClickHandler UI点击的调用。
IPointerUpHandler UI点击抬起时的调用。
IPointerDownHandler UI点击按下时的调用。
那为什么不用一个 IPointerHandler 来处理三个呢?就是这个原则了,尽量小而精,而非大而全。
面向接口编程而非面向实现(切换、并行开发)
客户端通过一系列抽象操作实例,而无需关注具体类型。
便于灵活切换一系列功能。
实现软件的并行开发。
排序的例子。
迪米特法则(类与类交互的原则)
Law of Demeter
不要和陌生人说话。
类与类交互时,在满足功能要求的基础上,传递的数据量越少越好。 因为这样可能降低耦合度。
语法:在子类中使用override关键字修饰的方法。
作用:父类的方法在子类中不适用(虚方法),或父类没有实现(抽象方法)。子类重写可以满足对该方法的不同需求。方法重写时必须在方法前加override关键字。
不管通过父类还是子类型的引用,调用方法时,都执行对象真实类型中定义的方法。
如图。每个类,在内存的堆中,都有一个方法表。
重写的原理就是,实际运行时,把子级的方法表中的方法地址,覆盖掉父级方法地址,然后实际父类引用调用父类方法时,就会是子类的方法了。
因为这种覆盖是随着程序进行而会更改的,比如调用A子类,A子类先覆盖,然后调用。然后到B子类,就变成B子类覆盖,然后调用。
这种,在运行时修改的,也叫动态绑定。
我一直以来认为,既然继承了父类,那么我也有相同的方法,并且此方法应该包含父类的逻辑,这才算继承把?
目前老师的回答是。需要的时候就调用,不需要的时候就不调用。
比如,我只想调用某个父类的父类的相同方法,这应该如何调用?
// (virtual/absctrac -> override)
// 重写。只是为了父类引用可以调用到子类的方法。
// 是否使用父类方法的逻辑,得用
base.xxxx();
定义:在子类中使用new关键字修饰的与父类同签名的方法。
作用:父类的方法在子类中不适用,且通过子类型引用调用时,隐藏掉父类继承的旧方法,好像该方法不存在。
子类在自己的方法表中增加一个新地址。
比如
C { call(); }
A extend C{ call(); }
B extend C{
virtual call();
}
toCall(C temp){
temp.call();
}
toCall(B) --> 用的是B类的Call,而不是C类的Call
toCall(A) --> 在toCall里,用的不是A类的call,而是C类的Call
想要使用A类的Call,要改成
toCall(C temp){
(temp as A).call();
}
所以说,如果是方法隐藏,那么“引用类型”是什么,那就只会调引用类型的方法,不会因为传入的是子类,就调用子类的方法。
定义:用vritual关键修饰的已实现方法。
作用:可以在子类中重写的方法。
绑定:类型与关联的方法的调用关系,通俗讲就是一个类型能够调用哪些方法。(内存中有张表)
静态绑定:是指调用关系是在运行之前确定的,即编译期间。
动态绑定:是指调用关系是在运行期间确定的。
动态绑定因为在运行期确定,占用运行时间,但是更灵活。
优先选择方法覆盖,因为是静态绑定,速度更快。
接口定义一组对外的行为规范,要求它的实现类必须遵循。
接口只关注行为,不关注数据,且不关注行为的实现,实现由实现类完成。
接口自身表达“能够做”,不表达“如何做”。
接口是一组行为的抽象,它的方法没有方法体,也就是自己不写方法的逻辑。
//一组:接口中可以包含多个方法;
//对外:接口成员是要求子类实现,自己不要用。
//行为:接口中只能包含方法成员(属性、方法)
//规范:要求子类必须自行实现
抽象类与子类之间关系:is a [是一种]。内部的属性等,子类都可以直接用。
接口与实现类之间关系:can do [能够做(功能)]。内部的东西不可以直接用,必须再次实现。
接口与接口之间可继承,且可以多继承。
类与类是单继承,类与接口是多实现,接口与接口是多继承。
父类,只能有一个。而接口,可以很多个。
当都可以用时,优先选择接口。
规范不同类型的行为,达到了不同类型在行为上是一致的。
比如,伤害。
玩家和敌人,肯定都能受到伤害,那么它们可以继承同个父类。
但是如果,树木,房子等也可以受到伤害,那就不能用同个父类了。
扩展一个已有类的行为。
或者在设计的后期,一个类以及设计并且实现完毕,这时候需要增加一个功能,一般也会用接口。
比如两个接口,都有同一个名称的方法。这时候如何实现,如何调用呢?
IComparable 可比较,使类型支持比大小的功能
如图,使用Array.Sort 进行排序时,Grenade需要实现 IComparable 接口的 CompareTo 方法才可以,不然会报错。也就是说,Array的排序功能,是通过 IComparable 接口,这个功能实现的。
IComparer 比较器,提供比较的方法,常用于排序比较
IEnumerable 可枚举,使类型支持简单迭代(foreach)
IEnumerator 枚举器,支持MoveNext ,自己可以控制迭代的节奏
IComparable
Array.Sort(array)
IComparable 的逻辑,是Sort里直接调用传入的类的CompareTo,写在类里。所以,如果这个类大部分都用这种方法排序,那么就使用 IComparable。
IComparer
Array.Sort(array, new XXXComparer);
IComparer 是需要在使用Sort时,new一个传入的,所以一般不会写在array代表的类里,它更灵活,有点像策略模式。但这样就说明它这个类的排序方式多变。
综上,这两个其实可以结合使用。常用的放类里,不常用/特殊的 放外面。
由上往下,耦合度越低,抽象程度越低,功能也越简单。
foreach(var item in hand) {
}
//foreach 的原理,就是如下的代码
// 1.获取迭代器
IEnumerator iter = hand.GetEnumerator();
// 2.移动到下一个元素
while(iter.MoveNext() ) {
// 3.获取元素
Console.WriteLine(iter.Current);
}
如上,foreach的原理如此。
public class Hand :IEnumerable{
public IThrowable[] AllObject { get; set; }
public IEnumerator GetEnumerator() {
return new HandEnumerator() {
Target = AllObject
};
}
public void Thorwing(IThrowable temp) {
temp.Fly();
}
}
// Hand的迭代器
public class HandEnumerator : IEnumerator {
public IThrowable[] Target { get; set; }
private int index = -1;
// 获取当前数据
public object Current{
get {
return Target[index];
}
}
public bool MoveNext() {
index++;
return index < Target.Length;
}
public void Reset() {
throw new NotImplementedException();
}
}
实现方式,就是类继承,IEnumerable,代表此类有这个功能。
子类实现一个迭代器类,继承 IEnumerator ,代表它是一个迭代器类,并且从父类传入需要迭代的“队列”,由此实现的接口代码才是实际的逻辑。父类也只是返回一个自己的迭代器类。
我们重新实现一下 GetEnumerator 的逻辑
public class Hand :IEnumerable{
/*
* 传统代码
public IEnumerator GetEnumerator() {
return new HandEnumerator() {
Target = AllObject
};
}
*/
public IEnumerator GetEnumerator() {
/*
将 yield 以前的代码,分配到 MoveNext 方法中
将 return 后的数据,分配到 Current 中
*/
for(int i = 0; i < AllObject.Length; i++) {
yield return AllObject[i];// 返回数据, 退出方法
}
}
}
还是看之前,迭代器的实现,这里改成用 yield。
//foreach 的原理,就是如下的代码
// 1.获取迭代器
IEnumerator iter = hand.GetEnumerator();
// 2.移动到下一个元素
while(iter.MoveNext() ) {
// 3.获取元素
Console.WriteLine(iter.Current);
}
我们看实际foreach的逻辑代码,不同点在这段代码
IEnumerator iter = hand.GetEnumerator();
它并不会实际执行,而是在
while(iter.MoveNext() )
中才会跳到
for(int i = 0; i < AllObject.Length; i++) {
yield return AllObject[i];// 返回数据, 退出方法
}
里,做第一次循环执行。
所以说:
public IEnumerator GetEnumerator() {
/*
将 yield 前的代码,分配到 MoveNext 方法中
将 return 后的数据,分配到 Current 中
*/
for(int i = 0; i < AllObject.Length; i++) {
yield return AllObject[i];// 返回数据, 退出方法
}
}
具有多个返回点(yield),可以在特定时机分部执行的函数。
private IEnumerator iter;
private void OnGUI() {
if(GUILayout.Button("启动")) {
iter = Fun1();
}
if(GUILayout.Button("执行一次")) {
iter.MoveNext();
}
if(GUILayout.Button("携程")) {
//StartCoroutine(iter);
//每帧调用一次 MoveNext 方法
//相当于
StartCoroutine( Fun1() );
}
}
private IEnumerator Fun1() {
for(int i = 0;i < 5;i++) {
print(i + "--" + Time.frameCount);
//yield return null;
yield return new WaitForSeconds(1);
}
}
}
先附上所有代码
private IEnumerator Fun1() {
for(int i = 0;i < 5;i++) {
print(i + "--" + Time.frameCount);
//yield return null;
yield return new WaitForSeconds(1);
}
}
可以看到,fun1返回的是 IEnumerator 对象,也就是迭代器对象
StartCoroutine( Fun1() );
同时,用迭代器的模板也是可以执行的
if(GUILayout.Button("启动")) {
iter = Fun1();
}
if(GUILayout.Button("执行一次")) {
iter.MoveNext();
}
唯一的不同在于, StartCoroutine 不需要我们手动点击按钮执行MoveNext(),而是由C#自动控制的。
private IEnumerator Fun1() {
for(int i = 0;i < 5;i++) {
print(i + "--" + Time.frameCount);
yield return null;
}
}
携程默认是每个渲染帧(Time.frameCount)调用一次
private IEnumerator Fun1() {
for(int i = 0;i < 5;i++) {
print(i + "--" + Time.frameCount);
//yield return null;
yield return new WaitForSeconds(1);
}
}
在 return 后增加一个 new WaitForSeconds(1); 那么,每次的间隔都会变成 1s,如图。
Unity每帧处理GameObject中的协同程序(不是Component的协程),直到函数执行完毕。
流程
如此循环至整个函数结束。
通过MonoBehaviour中的StartCoroutine启动,StopCoroutine停止。
协程函数返回值类型为IEnumerator,方法体中通过yield关键字定义返回点,通过return xx对象定义继续执行的条件。
可以被yield return 的对象:
1.延时调用。
2.分解操作。
// 透明度变化
public float fadeSpeed;
public IEnumerator FadeOutTest() {
Color currentColor;
do {
currentColor = mt.color;
currentColor.a -= fadeSpeed * Time.deltaTime;
mt.color = currentColor;
yield return null;
} while(currentColor.a > 0);
currentColor.a = 0;
mt.color = currentColor;
}
// 颜色变化
public Color EndColor;
public AnimationCurve curve;
public IEnumerator FadeOut() {
Color oriColor = mt.color;
//for(float x = 0; x <= 1; x += Time.deltaTime) {
// 2s,变慢
for(float x = 0; x <= 1; x += Time.deltaTime/2) {
mt.color = Color.Lerp(oriColor, EndColor, curve.Evaluate(x) );
yield return null;
}
}
// 动画曲线:提供数值可视化的操作面板
// Color.Lerp 将数值的变化,变为颜色的变化
代码就不说了
AnimationCurve
动画曲线:提供数值可视化的操作面板。
上面的颜色变化中,x的最大值是1,也就是1s,那么要如何延长时间呢?如下代码:
//for(float x = 0; x <= 1; x += Time.deltaTime) {
// 2s,变慢
for(float x = 0; x <= 1; x += Time.deltaTime/2) {
Color.Lerp
Color.Lerp 将数值的变化,变为颜色的变化
nity3D中的线性插值Lerp()函数解析
//a1 b1 d1 f1 ...(2s)... c106 e106
private Coroutine coroutine;
private void Start() {
print("a: " + Time.frameCount);
coroutine = StartCoroutine( Fun1() );
print("d: " + Time.frameCount);
StartCoroutine(Fun2());
print("f:" + Time.frameCount);
}
private IEnumerator Fun1() {
print("b:" + Time.frameCount);
yield return new WaitForSeconds(2);
print("c:" + Time.frameCount);
}
// 这里return ciroutine 是指等待 coroutine 指向的那个协程一次运行结束(下个yield之前/协程运行完)
private IEnumerator Fun2() {
yield return coroutine;
print("e:" + Time.frameCount);
}
这里的顺序是 a1 b1 d1 f1 …(2s)… c106 e106
a1,这里的1代表是哪一帧,所以可以得出结论,在yield之前的操作是不会放到下一帧的。
public Transform[] wayPoints;
private float moveSpeed;
public IEnumerator PathFinding() {
for(int i = 0; i < wayPoints.Length; i++) {
//移动到目标点
yield return StartCoroutine( MoveToTarget(wayPoints[i].position) );
}
}
private IEnumerator MoveToTarget(Vector3 position) {
transform.LookAt(position);
while(Vector3.Distance(transform.position, position) > 0.1f) {
transform.position = Vector3.MoveTowards(transform.position, position, moveSpeed * Time.deltaTime);
yield return new WaitForFixedUpdate();
}
}
private void OnGUI() {
if(GUILayout.Button("Go")) {
StartCoroutine( PathFinding() );
}
}
主要是讲解分解操作,利用了协程
yield return new xxxcoroutine;
等待xxx协程执行完毕再继续,的特性。来分解寻路到每个点的步骤。
属性有public,private,protected等访问权限关键字。类也有
老师的说法是。这种设计模式,难度有些高,要在项目初期,由经验丰富的程序员来设计比较好。
因为这种模式的拓展并不是很好。
基本用在框架级的代码
动态获取类型信息,动态创建对象,动态访问成员的过程。
在编译时无法了解类型,在运行时获取类型信息,创建对象,访问成员。
具体实现流程,请看 4.
Activator.CreateInstance(string 程序集名称,string 类型全名)
Activator.CreateInstance(Type type);
Assembly assembly = Assembly.Load(程序集);
assembly.CreateInstance(Type);
//找到有参构造方法,动态调用构造方法
type.GetConstructor(typeof(string)).Invoke()
// 编译时
User user1 = new User();
user1.ID = 1001;
user1.LoginID = "zs";
user1.Print();
// 动态 ---> 运行时
// 获取 Type
// -- 根据字符串获取类型
Type type = Type.GetType("Day5.User");//命名空间.类名
// 目的:这样我们完全可以把 需要使用的类,放在表里,让使用的人来选择
//Type type = Type.GetType( Console.ReadLine() );
// -- 根据对象获取类型
//Type type = user1.GetType();
// -- 根据数据类型
// Type type = typeof(User);
// 创建对象
object instance = Activator.CreateInstance(type);
// 访问成员
PropertyInfo IDProperty = type.GetProperty("ID");
// 当你确定使用的这系列类,一定由这个 属性 的 类型 时
IDProperty.SetValue(instance, 1001);
// 当你不确定使用的这系列类,的某个 属性 的 类型 时
object idValue = Convert.ChangeType("1001", IDProperty.PropertyType);
IDProperty.SetValue(instance, idValue);
PropertyInfo LoginIDProperty = type.GetProperty("LoginID");
LoginIDProperty.SetValue(instance, "zs");
// 获取方法
MethodInfo printMethod = type.GetMethod("Print");
// 调用方法
printMethod.Invoke(instance, null);
这里上面的 User 代码,和 下面的一长串代码,最后得到的是一样的 User。
不同的是,上面的User是写代码的时候,就会知道的类型,而下面的 “反射” 写法,则是运行时才会知道具体是什么类。
// -- 根据字符串获取类型
Type type = Type.GetType("Day5.User");//命名空间.类名
// 目的:这样我们完全可以把 需要使用的类,放在表里,让使用的人来选择
//Type type = Type.GetType( Console.ReadLine() );
把上面和下面代码对比一下,就知道它的用法了。
比如,游戏中的buff。这时候,可以在表里填写这一类型的类名和参数,这样就可以灵活变化和测试,到底应该使用那些buff。
// 获取方法
MethodInfo printMethod = type.GetMethod("Print");
// 调用方法。null代表无参数。object[] 是参数按要求
printMethod.Invoke(instance, null);
// 访问成员
PropertyInfo IDProperty = type.GetProperty("ID");
// 当你确定使用的这系列类,一定由这个 属性 的 类型 时
IDProperty.SetValue(instance, 1001);
// 当你不确定使用的这系列类,的某个 属性 的 类型 时
object idValue = Convert.ChangeType("1001", IDProperty.PropertyType);
IDProperty.SetValue(instance, idValue);
我们可以不知道属性的类型,如何做请看代码。
这里有个,不灵活的地方,就是属性名称。我们可以不知道属性的类型,但是名称是一定要知道的。
public static string Object2Json(object obj) {
// 获取所有属性(名称/值)
// 根据规则拼接字符串
// 提示:在MSDN中 搜索 "StringBuilder"类
Type type = obj.GetType();
PropertyInfo[] allProperty = type.GetProperties();
StringBuilder builder = new StringBuilder();
builder.Append("{");
foreach(var item in allProperty) {
builder.AppendFormat("\"{0}\":\"{1}\",", item.Name, item.GetValue(obj) );
}
builder.Remove(builder.Length - 1, 1);
builder.Append("}");
return builder.ToString();
}
public static T Json2Object(string json) where T:new(){
// 创建对象
// 字符串解析(提取 属性名、名称值)
// 根据属性名 设置属性
// 提示:在 MSDN 中搜索"String"类
//Type type = typeof(T);
//object instance = Activator.CreateInstance(type);
T instance = new T();
Type type = instance.GetType();
json = json.Replace("\"", "").Replace("{", "").Replace("}", "");
//json.Replace("\"", string.Empty);
string[] keyValue = json.Split(':','c');
for(int i = 0;i < keyValue.Length - 1; i++) {
PropertyInfo property = type.GetProperty(keyValue[i]);
property.SetValue(instance, Convert.ChangeType(keyValue[i + 1], property.PropertyType) );
}
return instance;
}
反射目前都是通过 类型 去找属性的,而不是通过 对象。
builder.Append("{");
foreach(var item in allProperty) {
builder.AppendFormat("\"{0}\":\"{1}\",", item.Name, item.GetValue(obj) );
}
builder.Remove(builder.Length - 1, 1);
builder.Append("}");
主要就是这段。没啥好说的。
主要看这里
// 使用 replace 把所有 “ { } 都替换为 空字符串。
// 可以得到 只剩 “key:value,key:value,key:value” 这样的字符串
json = json.Replace("\"", "").Replace("{", "").Replace("}", "");
//json.Replace("\"", string.Empty);
// 按 :和 ,把 key 和 value 都单独出来,农场队列。
// 可以得到 。{key,value,key,value, ...} 这样的队列
string[] keyValue = json.Split(':','c');
public static DaoFactory Instance {
//如果增加新的存储方式,违反开闭原则
//选择子类
get {
if(GameMain.Type == "Client") {
return new ClientDaoFactory();
} else {
return new ServerDaoFactory();
}
//
if(null == instance) {
// 动态(利用字符串)创建对象
// 定义规则:GameMain.Type + Factory
Type type = Type.GetType(GameMain.Type + "Factory");
//return Activator.CreateInstance(type) as DaoFactory;
instance = Activator.CreateInstance(type) as DaoFactory;
}
// www.......com/user/login? loginID&zs
// UserHandle
return instance;
}
}
这里用反射,重写了之前的抽象工厂方法。
老师的意思是说,这里在开发中,属于并行开发,我们不知道其他程序会写出哪些工厂,所以用反射来灵活的配置。
因为反射很耗时间,所以还用了单例的方法,并且列出了规则。
如图,在编辑器的如图路径下,保存着Unity所有的模板。
这里的文件名称什么意思呢?
81-C# Script-NewBehaviourScripts.cs
如下图。
比如我们常用到的mono模板,可以看到和编辑器里看到的是不一样的。#SCRIPTNAME#是我们的类名称,要等创建的时候才会赋值过去。
public class MonoSingleton : MonoBehaviour where T:MonoSingleton{
// T 表示子类类型
private static T instance;
public static T Instance {
get {
if(instance == null) {
instance = FindObjectOfType();
if(instance == null) {
//创建脚本对象
instance = new GameObject("Singleton of " + typeof(T)).AddComponent();
} else {
instance.init();
}
}
return instance;
}
}
protected void Awake() {
if(instance == null) {
instance = this as T;
init();
}
}
public void init() {
}
/*
* 备注:
* 1.适用性:场景中存在唯一的对象,即可让该对象继承当前类
* 2.如何适用:
* -- 继承时必须传递子类类型
* -- 在任意脚本生命周期中,通过子类类型访问Instance属性
*/
老师给了一个,Unity的单例基类,继承于它的,都可以使用单例模式。
一般我是把单例在开始的场景中直接挂上,然后dontdestory,就可以一直存在了。老师这种,如果没有挂上,则会动态在场景中创建对象。
public class MonoSingleton< T >
这里的T和函数里的T都表示一个未知类,可以让传入,让整个系列的类,使用自己传入的类型来生成属性,以及使用。
public class MonoSingleton : MonoBehaviour where T:MonoSingleton
where 后面就代表约束,具体语法我也不是很清楚,看msdn去。
where T:MonoSingleton
这段表示的是,T是继承于MonoSingleton
约束告知编译器类型参数必须具备的功能。 在没有任何约束的情况下,类型参数可以是任何类型。
public static T Instance {
get {
if(instance == null) {
instance = FindObjectOfType();
if(instance == null) {
//创建脚本对象
instance = new GameObject("Singleton of " + typeof(T)).AddComponent();
} else {
instance.init();
}
}
return instance;
}
}
首先,肯定是判重拉,如果没有instance,得先在世界中查找
FindObjectOfType();
如果还是没找到,那我们就要创建 UnityObject 然后 AddComponent< T >
//创建脚本对象
instance = new GameObject("Singleton of " + typeof(T)).AddComponent();
protected void Awake() {
if(instance == null) {
instance = this as T;
init();
}
}
public void init() {
}
这是老师的做法:
一般来说,只要生成一个 component 后,一定会执行 awake。而我们为保证在没有第一次调用 Instance 而走到Awake,就在 Awake 里也加了预防代码。
一般都用 Awake做初始化,现在改用了 Init 来做初始化。并且使用 protected 来限制它的访问级别。
其实暂时还不太理解init的这种写法,等以后实际使用时再慢慢消化吧。