0%

c#中单例模式的实现#

本质上,单例模式是一个只允许创建一个实例,并提供对这个实例简单的访问途径的类。一般而言,单例模式在创建实例时不允许传递任何参数,否则不同参数导致不同的实例创建,就会出现问题!(如果同一个实例可以被同参的不同请求所访问,那么工厂模式会更适合。)只有在第一次被用到的时候才会被创建。在C#中有实现单例模式有很多种方法。我展示如下三个版本:从线程不安全的到简单线程安全的、再到简洁高效的版本。所有的这些实现都有以下四个特征:

  1. 只有一个构造函数,而且是私有的,不带参数的。这是为了防止其他类对其实例化,同时也防止了子类化--如果一个单例能被子类化 一次,就能被子类化两次,而如果每个子类可以创建一个实例,这与模式本身又产生了冲突。如果你(遇到这样的情况):只有在运行期才能知道实际的类型,因此需要一个父类的单例,可以使用工厂模式。
  2. 类是密封的(即不可以被继承的类)。这并不是必须的,严格的说,即如上一点所说的原因,可以提高JIT(Just-In-Time , 运行时编译执行的技术)的效率。
  3. 一个静态变量用来保存单例的引用
  4. 一个用以访问单例引用的公用静态方法
版本1-非线程安全#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

// Bad code! Do not use!
public sealed class Singleton
{
static Singleton instance=null;
Singleton()
{
}
public static Singleton Instance
{
get
{
if (instance==null)
{
instance = new Singleton();
}
return instance;
}
}
}

两个线程可能同时判断“if (instance==null)”,发现为TRUE,于是都创建了实例,这又违背了单例模式。无法确保只能让一个线程使对象实例化

版本2- 简单的线程安全#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public sealed class Singleton
{
static Singleton instance=null;

static readonly object padlock = new object();
Singleton()
{
}
//每次访问的时候都要进行锁定
public static Singleton Instance
{
get
{
//共用的对象进行锁定,然后判断实例是否在之前已经创建。
lock (padlock)
{
if (instance==null)
{
instance = new Singleton();
}
return instance;
}
}
}
}

这个实现是线程安全的。线程首先对共用的对象进行锁定,然后判断实例是否在之前已经创建。不幸的是,由于在每次访问的时候都要进行锁定,所以影响了性能。(这对于多 线程并发的高性能要求的应用显得尤为重要)。对那些可被其他类访问的对象进行锁定或对类型进行锁定会导致性能问题甚至引起死锁

版本3- 用双重检测机制的错误版本(没用公用只读对象)#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
//sealed关键字,表示不能被继承
public sealed class Singleton
{
static Singleton instance=null;
Singleton()
{
}
public static Singleton Instance
{
get
{
//先判断是否存在单例
if (instance==null)
{
lock (instance)
{
if (instance==null)
{
instance = new Singleton();
}
}
}
return instance;
}
}
}

这个是错误的,在c#中,lock空指针会报错。

版本4- 用双重检测机制的线程安全#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
//sealed关键字,表示不能被继承
public sealed class Singleton
{
static Singleton instance=null;
static readonly object padlock = new object();
Singleton()
{
}
public static Singleton Instance
{
get
{
//先判断是否存在单例
if (instance==null)
{
lock (padlock)
{
if (instance==null)
{
instance = new Singleton();
}
}
}
return instance;
}
}
}

这个实现是线程安全的。线程先判断是否存在单例,不存在实例才对共用的对象进行锁定,然后判断实例是否在之前已经创建。看似看似没有问题,但却会引发另一个问题,这个问题由指令重排序引起。指令重排序是为了优化指令,提高程序运行效率。指令重排序包括编译器重排序和运行时重排序。例如 instance = new Singleton() 可分解为如下伪代码:

排序前

1
2
3
memory = allocate();   //1:分配对象的内存空间
ctorInstance(memory); //2:初始化对象
instance = memory; //3:设置instance指向刚分配的内存地址

排序后

1
2
3
4
memory = allocate();   //1:分配对象的内存空间
instance = memory; //3:设置instance指向刚分配的内存地址
//注意,此时对象还没有被初始化!
ctorInstance(memory); //2:初始化对象
由于并行执行,则线程A执行了instance = memory然后线程B检查到instance不为null,则会使用未经实例化完全的对象进行操作,引发错误。

版本5- 用双重检测机制的线程安全(加入volatile)#

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// Bad code! Do not use!

public sealed class Singleton
{
private volatile static Singleton instance = null;//volatile 可以禁止
private static readonly object padlock = new object();

Singleton()
{
}

public static Singleton Instance
{
get
{
if (instance == null)
{
lock (padlock)
{
if (instance == null)
{
instance = new Singleton();
}
}
}
return instance;
}
}
}

volatile表明属性将被多个线程同时访问,告知编译器不要按照单线程访问的方式去优化该字段,线程会监听字段变更,但是不保证字段访问总是顺序执行

版本6- 线程安全,避免实例创建和引用赋值混乱#

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
public sealed class Singleton
{
private static Singleton instance = null;//volatile 可以禁止
private static readonly object padlock = new object();

Singleton()
{
}

public static Singleton Instance
{
get
{
if (instance == null)
{
lock (padlock)
{
if (instance == null)
{
var tmp= new Singleton();
// ensures that the instance is well initialized,
// and only then, it assigns the static variable.
System.Threading.Thread.MemoryBarrier();
instance = tmp;
}
}
}
return instance;
}
}
}

官方文档是说Thread.MemoryBarrier(),保证之前的数据存取优先于MemoryBarrier执行,只有在多CPU下才需要使用

版本7- 不用锁实现线程安全,不全是延时初始化#

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public sealed class Singleton
{
private static readonly Singleton instance = new Singleton();

// Explicit static constructor to tell C# compiler
// not to mark type as beforefieldinit
static Singleton()
{
}

private Singleton()
{
}

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

C#中的静态构造函数被指定仅在创建类的实例或引用静态成员时执行,并且每个AppDomain只执行一次,所以是延时初始化,但是当Singleton有多个静态属性,且其他的属性被访问时,实例也会被初始化,所以不是完全的延时初始化方式

版本8- 不用锁实现线程安全,不全是延时初始化#

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public sealed class Singleton
{
private Singleton()
{
}

public static Singleton Instance { get { return Nested.instance; } }

private class Nested
{
// Explicit static constructor to tell C# compiler
// not to mark type as beforefieldinit
static Nested()
{
}

internal static readonly Singleton instance = new Singleton();
}
}

只有当Nested类的第一个引用调用时,Singleton才会被实例化,也就是调用Instance,是完全的延时初始化

版本9- 线程安全, .NET 4之后支持#

1
2
3
4
5
6
7
8
9
10
11
public sealed class Singleton
{
private static readonly Lazy<Singleton> lazy =
new Lazy<Singleton>(() => new Singleton());

public static Singleton Instance { get { return lazy.Value; } }

private Singleton()
{
}
}

观察者模式#

Model-View-Controller架构就是观察者模式的例子,C#中的Event委托也是使用的观察者模式

##### 进度条更新初阶版本
下面样例产生了编译时的依赖(违背了抽象依赖细节)应该依赖抽象基类(MainForm与ProgressBar之间产生了依赖)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
class FileSplitter
{
string m_filePath;
int m_fileNumber;
ProgressBar* m_progressBar;

public:
FileSplitter(const string& filePath, int fileNumber, ProgressBar* progressBar) :
m_filePath(filePath),
m_fileNumber(fileNumber),
m_progressBar(progressBar){

}

void split(){

//1.读取大文件

//2.分批次向小文件中写入
for (int i = 0; i < m_fileNumber; i++){
//...
float progressValue = m_fileNumber;
progressValue = (i + 1) / progressValue;
//更新进度条
//不能依赖实现细节
m_progressBar->setValue(progressValue);
}
}

class MainForm : public Form
{
TextBox* txtFilePath;
TextBox* txtFileNumber;
//依赖ProgressBar
ProgressBar* progressBar;

public:
void Button1_Click(){

string filePath = txtFilePath->getText();
int number = atoi(txtFileNumber->getText().c_str());

FileSplitter splitter(filePath, number, progressBar);

splitter.split();
}
};

文件读取进度条更新观察者模式改进版本#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
//修改后,将progressbar抽象成接口,紧耦合变成松耦合,编译就不依赖progress
//变成了良好的耦合,符合依赖倒置原则
//iprogress接口
class IProgress{
public:
virtual void DoProgress(float value)=0;
virtual ~IProgress(){}
};


//被观察对象拥有一个观察者对象的集合(相当于subject)
class FileSplitter
{
string m_filePath;
int m_fileNumber;
//改良:使支持多个观察者,加入vector 或者list等
List<IProgress*> m_iprogressList; // 抽象通知机制,支持多个观察者

public:
FileSplitter(const string& filePath, int fileNumber) :
m_filePath(filePath),
m_fileNumber(fileNumber){
}

void split(){
//1.读取大文件
//2.分批次向小文件中写入
for (int i = 0; i < m_fileNumber; i++){
//...
float progressValue = m_fileNumber;
progressValue = (i + 1) / progressValue;
onProgress(progressValue);//发送通知
}
}

//原本列表初始化已经不适用,添加add和remove操作函数
void addIProgress(IProgress* iprogress){
m_iprogressList.push_back(iprogress);
}

void removeIProgress(IProgress* iprogress){
m_iprogressList.remove(iprogress);
}

//继续优化,protected下写onProgress,层次更清晰
protected:
//对于onprogress也需要改变
virtual void onProgress(float value){
List<IProgress*>::iterator itor=m_iprogressList.begin();
while (itor != m_iprogressList.end() )
(*itor)->DoProgress(value); //更新进度条
itor++;
}
}
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
class ConsoleNotifier : public IProgress {
public:
virtual void DoProgress(float value){
cout << ".";
}
};


class MainForm : public Form, public IProgress
{
TextBox* txtFilePath;
TextBox* txtFileNumber;

ProgressBar* progressBar;

public:
void Button1_Click(){

string filePath = txtFilePath->getText();
int number = atoi(txtFileNumber->getText().c_str());

ConsoleNotifier cn;

FileSplitter splitter(filePath, number);
//添加监听
splitter.addIProgress(this); //订阅通知
splitter.addIProgress(&cn); //订阅通知

splitter.split();

splitter.removeIProgress(this);

}
//重写DoProgress
virtual void DoProgress(float value){
progressBar->setValue(value);
}
};

享元模式#

内存池,对象池的实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
//基本类
class Font {
private:
//unique object key
string key;
//object state
//....

public:
Font(const string& key){
//...
}
};
ß

//建立一个工厂
class FontFactory{
private:
map<string,Font* > fontPool;

public:
Font* GetFont(const string& key){

map<string,Font*>::iterator item=fontPool.find(key);

if(item!=footPool.end()){
return fontPool[key];
}
else{
Font* font = new Font(key);
fontPool[key]= font;
return font;
}

}

void clear(){
//...
}
};

命令模式#

命令就是面向对象化的回调。

示例代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
#include <iostream>
#include <vector>
#include <string>
using namespace std;


//基类
class Command
{
public:
virtual void execute() = 0;
virtual ~Command(){}
};


class ConcreteCommand1 : public Command
{
string arg;
public:
ConcreteCommand1(const string & a) : arg(a) {}
void execute() override
{
cout<< "#1 process..."<<arg<<endl;
}
};

class ConcreteCommand2 : public Command
{
string arg;
public:
ConcreteCommand2(const string & a) : arg(a) {}
void execute() override
{
cout<< "#2 process..."<<arg<<endl;
}
};


class MacroCommand : public Command
{
vector<Command*> commands;
public:
void addCommand(Command *c) { commands.push_back(c); }
void execute() override
{
for (auto &c : commands)
{
c->execute();
}
}
};



int main()
{
ConcreteCommand1 command1(receiver, "Arg ###");
ConcreteCommand2 command2(receiver, "Arg $$$");

MacroCommand macro;
macro.addCommand(&command1);
macro.addCommand(&command2);

macro.execute();
}

命令模式很容易实现撤销和重做

软件架构#

  1. 衡量一个设计的好坏的方法就是 看它应对变化的灵活性
  2. 许多模式让代码更灵活,但它依赖于虚函数派发,接口,指针,消息以及其它一些至少有一些运行成本的机制。
  3. 保持代码的灵活性,直到设计稳定下来,然后去除一些抽象,提高游戏性能。

开发中的考虑因素 1. 良好的框架,更容易理解代码 2. 快速的运行时性能 3. 快速完成今天的功能

1.常见架构#

  • EmptyGo
  • simple GameManager
  • Manager of Managers
  • MVCS
  • MVVM:UFRAME

2.EmptyGo#

  • Put all the code without visual representation in the world onto an empty game object.
  • use GameObject.Find() or inspector target references to communicate with each other.GameObject.Find()不解耦合

3.simple GameManager#

  • 改为一个singleton
  • 包括UI设计,包括模块的访问
  • 全部塞进去,会发生混乱

4.Manager of Managers(中型游戏非常常用)#

  • MainManager(customizes and managers all the submanagers)(submanagers operate as singletons and can easily address each other to collaborate
    1. EventManager:集中管理UI到各个模块之间的消息,以及各个模块相互访问的传递
    2. AudioManager:在场景中任何一个地方播放音乐,都用它来管理
    3. GUIManager:管理所有UI发生的click的事件
    4. PoolManager:把已经初始化,但暂不使用的gameobject放入pool(非常重要)
    5. LevelManager:关卡管理(非常重要)
    6. GameManager:
    7. SaveManager:load page,游戏退出后,进入还想在原处(非常重要)
    8. MenuManager:管理Menu上的动画,外观上的东西(严格与GUIManager事件管理区分开来)

4.1 level manager#

4.1.1 Unity 自带 Application.loadLevel();#

ISSUE 1: you need to know the scene name or the index of the scene you want to load,but most probably the name or order will be changed later.

1
2
Application.loadLevel("FirstLevel");
Application.loadLevel(1);

ISSUE 2: there's no simple method of passing arguments to a scene , e.g., assuming you're resuing one scene for many different levels.

ISSUE 3:managing multiple level work flows is not a simple task. suppose you want to create two versions of your game:

1
2
3
4
5
6
7
8
9
Application.loadLevel(1);
Application.loadLevel(2);
Application.loadLevel(3);
---
VS
---
Application.loadLevel(1);
Application.loadLevel(3);
Application.loadLevel(2);

4.2 level manager设计#

  1. Compose a configuration table.(需要变换加载场景顺序,仅需要在表内变换)
  2. create a new API
    • LevelManager.LoadNext();
  3. In the configuration table, it also should be allowed to set an argument line for each level.
  4. It should be allowed to create multiple configuration tables,and before building your appliaction switch between them.(示例:Unity商店MadLevelManager)

4.3 pool manager设计#

  1. Maintain a list of dormant objects in the pool class:
    1
    private List<GameObject> dormantObjects = new List <GameObject>();
  2. the list contains all different types of game objects / prefabs

SPAWN()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public GameObject Spawn(GameObject go){
GameObject temp = null;
if (dormantObjects.count>0){
foreach(GameObject dob in dormantObjects){
if(dob.name == go.name){
temp = dob;
dormantObjects.remove(temp);
return temp;
}
}
}
temp = GameObejct.Instantiate(go) as GameObejct;
temp.name = go.name;
return temp;
}

DESPAWN()

1
2
3
4
5
6
public GameObject Despawn(GameObject go){
go.transform.parent = PoolManager.transform;
go.SetActive(false);
dormantObjects.Add(go);
Trim();
}

TRIM(超过限制就删除)

1
2
3
4
5
6
7
public void Trim(){
while(dormantObjects.Count>Capacity){
GameObject dob = dormantObjects[0];
dormantObjects.RemoveAt(0);
Destory(dob);
}
}

PROBLEMS 1. This pool is not able to manage the Load/Unload of prefabs. 2. Only dormant objects are managed in the pool, active objects must be managed out of pool separately. 3. The total number of dormant objects can be controlled, rather than the instances of each prefabs.(总物体有数量限制,而不是每中类型没有限制)

better design PoolManager:Singleton,Manage multiple SpawnPools - SpawnPool1 + prefabPool1---Prefab1 + prefabPool2---Prefab2 - SpawnPool2 + prefabPool3---Prefab3 + prefabPool4---Prefab4

Spawnpool:design rules for spawnpool 1. define an empty object,set its transform as the parent of all the instances in the pool.(空物体) 2. manager multiple prefabpools via a dictionary

prefabPool 1. create a prefabPool for each prefab. 2. maintains a list of activated objects and another list of deactive objects. 3. centrally manage the load/unload process here.

4.4 save manager设计#

  1. save and load user preferences and achievements.
    • quit/resume game
  2. a lot of developers are used to save/load data with JSON/XML files.
  3. Is there any better solution?
    • Serialize almost any type of data.
    • it's fast,even on mobile devices.
    • encrypt save date efficiently and securely.
    • snap a screen

5. MVCS:STRANGEIOC(UI和逻辑分开)#

5.1 strangeioc framework#

  1. The core of the framework is binding
  2. basic structure: The key triggers the value.
    1
    IBinder.Bind<Key>().To<Value>();
  3. advanced structure: the name is a discriminator
    1
    IBinder.Bind<Key>().To<Value>().ToName(name);
  4. Type of key
key value Notes
event callback an event triggers a callback
interface implementation binds an interface to its implementation
class dependent class the instantiation of one class triggers the instantiation of dependent class.
  1. a view only does display and input.(UI事件和GameObject上的可视化相关)
  2. the mediator connects the view with the rest of your app
  3. commands are classes triggered by events.

5.2 Mediator#

Binding

1
mediationBinder.Bind<Example View>().To<ExampleMediator>();

Dispatcher - simple format

1
dispatcher.Dispatcher(AttackEvent.FIRE_MISSILE);
- Event + data
1
2
Vector3 orientation = gameObject.transform.loaclRotaion.eulerAngles;
dispatcher.Dispatcher(AttackEvent.FIRE_MISSILE, orientation);

Listener - If Listener is a method

1
2
dispatcher.AddListener(AttackEvent.FIRE_MISSILE, onMissileFire);
dispatcher.RemoveListener(AttackEvent.FIRE_MISSILE, onMissileFire);
- If Listener is a command
1
2
3
4
5
6
commandBinder.Binder(GameEvent.GUN_FIRE,OnGunFireCommand);
class OnGunFireCommand:EventCommand{
override public void Execute(){
...
}
}

5.3 Limitation#

  • Injection empolys reflection, which is slow.(反射机制耗时)
  • if inject sth,you have to map it, otherwise, it will result in null pointer errors.(容易造成空指针)

6 MVVM:UFRAME#

View--->ViewModel--->Model

7 编码策略#

  • 文件命名规范
  • 文件名分类
  • 用空物体来管理同类物体
  • 零容忍warnings and errors
  • 零容忍runtime memory location(动态开辟数组等)

感知机#

输入为实例的特征向量,输出为实例的类别,取+1和-1;

感知机对应于输入空间中将实例划分为 正负两类的分离超平面,属于 判别模型;

导入 基于误分类的损失函数;

利用 梯度下降法对损失函数进行极小化;

感知机学习算法具有简单而易于实现的优点,分为 原始形式和对偶形式;

1957年由Rosenblatt提出,是 神经网络与支持向量机的基础

定义#

1
f(x)=sign(w*x+b)  

几何解释: 线性方程 w*x+b,=0 对应于超平面S,w为法向量,b为截距

定义损失函数#

算法#

对偶性#

这个专题主要是总结一下读《统计学习》的一些知识点,这一章简单介绍一些机器学习的相关概念。

统计学习三要素#

1.模型 条件概率分布或决策函数#

2.策略#

  1. 损失函数和风险函数
  2. 经验风险最小化和结构风险最小化
  • 经验风险最小化:当样本足够大时,可以保证很好效果,但 样本小时,往往会 过拟合(loss均值)
  • 结构风险最小化(structural minimization,SRM):为防止过拟合提出的策略, 等价于 正则化(regularization) , 加入正则化项regularizer, 或罚项 penalty term。结构越复杂,正则化项(惩罚项)越大。

3.算法#

过拟合#

  • 若一味追求提高训练数据的预测能力, 所选模型的复杂度往往比真是模型要高,这种现象叫过拟合。
  • 过拟合也指学习时 选择的模型所包含的参数过多,以至于出现这一模型 对已知数据拟合很好,对于未知数据拟合很差。 可以说 模型选择旨在避免过拟合并且提高模型的预测能力

交叉验证#

训练集(训练数据)+ 验证集(模型选择)+ 测试集(测试)

泛化能力#

指该方法学习到的模型 对未知数据的预测能力,是学习方法本质上重要的性质。最多通过 测试误差来评价

比较学习方法的泛化能力:比较 泛化误差上界 性质: 样本容量增加, 泛化误差趋于0 假设空间容量越大, 泛化误差越大

生成模型和判别模型#

根据P(X,Y),求出条件概率分布P(Y|X)作为预测的模型,即生成模型P(Y|X)=P(X,Y)/P(X) (给定输入X,产生输出Y的关系)

生成方法: - 可还原出联合概率分布P(X,Y), 而判别方法不能。 生成方法的 收敛速度更快,当样本容量增加的时候, 学到的模型可以更快地收敛于真实模型; 当存在隐变量时, 仍可以使用生成方法, 而判别方法则不能用。 - 由数据直接学习决策函数f(x)或条件概率分布 P(Y|X)作为预测模型 (K近邻法、 感知机、 决策树、 logistic回归模型、 最大熵模型、 支持向量机、 提升方法和条件随机场。)

判别方法: - 直接学习到条件概率或决策函数,直接进行预测,往往学习的 准确率更高;由于直接学习Y=f(X)或P(Y|X),可对数据进行各种程度上的 抽象、 定义特征并使用特征, 因此可以简化学习过程。

回归问题#

回归学习最常用的损失函数是 平方损失函数, 在此情况下, 回归问题可以由 著名的 最小二乘法(least squares)求解。

标注问题#

监督学习,分类问题的推广,又是更复杂的结构预测(structure prediction)问题的简单形式。

标注: tagging, 结构预测: structure prediction 输入: 观测序列, 输出: 标记序列或状态序列 学习和标注(预测)两个过程