-
Notifications
You must be signed in to change notification settings - Fork 0
/
index.xml
375 lines (331 loc) · 64.1 KB
/
index.xml
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
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
<channel>
<title>宇宙边缘港</title>
<link>https://www.sooda.net.cn/</link>
<description>Recent content on 宇宙边缘港</description>
<generator>Hugo -- gohugo.io</generator>
<language>zh-CN</language>
<copyright>🌌</copyright>
<lastBuildDate>Wed, 31 Aug 2022 10:24:40 +0800</lastBuildDate><atom:link href="https://www.sooda.net.cn/index.xml" rel="self" type="application/rss+xml" />
<item>
<title>《CLR via C#》 托管堆和垃圾回收</title>
<link>https://www.sooda.net.cn/posts/clrviacsharp-gc/</link>
<pubDate>Wed, 31 Aug 2022 10:24:40 +0800</pubDate>
<guid>https://www.sooda.net.cn/posts/clrviacsharp-gc/</guid>
<description>托管堆基础 访问一个资源所需要的步骤:
调用IL指定newobj,为代表资源的类型分配内存(一般用C#new操作符来完成)。 初始化内存,设置资源的初始状态并使资源可用。类型的实例构造器负责设置初始化状态。 访问类型的成员来使用资源(有必要可以重复)。 摧毁资源的状态以进行清理。 释放内存。垃圾回收器独自负责这一步骤。 提示
只要是写可验证的、类型安全的代码(不使用C#unsafe),应用程序就不可能会出现内存被破坏的情况。但内存仍有可能泄露,但不像C/C++那样是默认行为。现在内存泄漏一般是因为在集合中存储了对象,但不需要对象的时候一直不删除。
从托管堆中分配资源 CLR要求所有对象都从托管堆分配。进程初始化时,CLR划出一个地址空间区域作为托管堆。CLR还要维护一个指针NextObjPtr,该指针指向下一个对象在堆中的分配位置。刚开始的时候NextObjPtr设为地址空间区域的基地址。
一个区域被非垃圾对象填满后,CLR会分配更到的区域。这个过程一直重复,直至整个进程地址空间都被填满。所以,应用程序的内存受进程的虚拟地址空间的限制。32位进程最多分配1.5GB,64位进程最多分配8TB。
new操作符的执行细节:
计算类型的字段(以及从基类型继承的字段)所需的字节数。 加上对象的开销所需的字节数。每个对象都有两个开销字段:类型对象指针和同步块索引。 CLR检查区域中是否有分配对象所需的字节数。如果托管堆有足够的空间,就在NextObjPtr指针指向的地址处放入对象,为对象分配的字节没被清零。接着调用类型的构造器(为this参数传递NextObjPtr),new操作符返回对象引用。在返回引用前,NextObjPtr指针的值会加上对象占用的字节数来得到一个新值,即下个对象放入托管堆时的地址。 托管堆的优点
对于托管堆,分配对象只需要在指针上加一个值(速度非常快)。在许多应用程序中,差不多同时分配的对象彼此间有较强的联系,而且经常差不多在同一时间访问。由于托管堆在内存中连续分配对象,所以因为引用“局部化”(locality)而获得性能上的提升。具体来说,这意味着工作集会非常小,应用程序只需要使用很少的内存,从而提高了速度。还意味着代码使用的对象可以全部驻留在CPU的缓存中。结果是应用程序能以非常快的速度访问这些对象,因为CPU在执行大多数操作时,不会因为“缓存未命中”(cache miss)而被迫访问较慢的RAM。
发挥此优点的前提是内存无限,但内存不可能无限,所以CLR用“垃圾回收”(GC)的技术“删除”堆中应用程序不再需要的对象。
垃圾回收算法 应用程序调用new操作符创建对象时,可能没有足够的空间来分配该对象。发现空间不够,CLR就执行垃圾回收(实际上,垃圾回收是在第0代满的时候发生的)。
引用计数算法 在引用计数算法中,堆上的每个对象都维护着一个内存字段来统计程序中有多少正在使用的对象。随着每一个对象到达代码中某个不再需要对象的地方,就递减对象的计数字段。计数字段变成0,对象就可以从内存中删除了。引用计数系统最大的问题就是处理不好循环引用。
引用跟踪算法 引用跟踪算法只关心引用类型的变量,因为只有这种变量才能引用堆上的对象;值类型变量直接包含值类型实例。引用类型变量可以在许多场合使用,包括类的静态和实例字段,或者方法的参数和局部变量。将所有引用类型的变量都称为根。
引用跟踪算法步骤:
CLR开始GC时,首先暂停进程中所有线程(防止线程在CLR检查期间修改对象的状态)。 CLR进入GC的标记阶段。CLR遍历堆中的所有对象,将同步块索引字段中的一位设为0,这编码所有对象都应删除。 CLR检查所有活动根,查看它们引用了哪些对象。如果一个根包含null,CLR忽略这个根并继续检查下一个根。任何根如果引用了堆上的对象,CLR都会标记那个对象,也就是将该对象的同步块索引中的位设置为1.一个对象被标记后,CLR会检查那个对象中的跟,标记它们引用的对象。如果发现对象已经标记,就不重新检查对象的字段。这就避免了应为循环引用而产生的死循环。 图1 回收之前的托管堆 图2 标记过后的托管堆 检查完毕后,堆中的对象要么已标记,要么未标记。已标记的对象不能被垃圾回收,因为至少有一个根在引用它。这种状态被称为可达的(reachable)。未标记的对象是不可达(unreachable)的。 CLR进入GC的压缩(compact)接端。这个接端,CLR对堆中已标记的对象进行移动,压缩幸存的对象,使它们占用连续的内存空间。但根所引用的位置还未改变,所以CLR需要从每个根减去所引用的对象在内存中偏移的字节数。这样就能保证每个根还是引用和之前一样的对象;只是对象在内存中变化了位置。 压缩的好处
首先,所有幸存对象在内存中紧挨在一起,恢复了引用的“局部化”,减小了应用程序的工作集,从而提升了将来访问这些对象时的性能。其次,可用空间也全部时连续的,允许其他东西进驻。最后,压缩意味着托管堆解决了本机(原生)堆的空间碎片化问题。
6. 压缩好内存后,托管堆的`NextObjPtr`指针指向最后一个幸存对象之后的位置。 图3 垃圾回收后的托管堆 注意
如果CLR在一个GC之后回收不了内存,而且进程中没有空间来分配新的GC区域,就说明进程的内存已耗尽。此时,试图分配更多的内存的new操作符就会抛出OutOfMemoryException。
注意
静态字段引用的对象一直存在,直到用于加载类型的AppDomain卸载为止。内存泄漏的一个常见原因就是让静态字段引用某个合集对象。静态对象会使合集对象一直存存活,而合集对象使所有的数据项一直存活。从而导致内存泄漏。
代:提升性能 CLR的GC是基于代的垃圾回收器(generational garbage collector),它对代码做出了几点假设。
对象越新,生存期越短。 对象越老,生存期越长。 回收堆的一部分,速度快于回收整个堆。 托管堆在初始化时不包含对象。添加到堆的对象称为第0代对象。简单地说,第0代对象就是那些新构造的对象,垃圾回收器从未检查过它们。
图4 新初始化的堆,所有对象都是第0代,垃圾回收尚未发生 CLR初始化时为第0代对象选择一个预算容量(KB单位)。如果分配一个新对象造成第0代超过预算,就必须启动一次垃圾回收。假设上图A到E正好使用完第0代空间,分配对象F时就必须启动垃圾回收。垃圾回收器会判断对象C和E时垃圾,所以会压缩D,使之与对象B相邻。在垃圾回收中存活的对象(A,B和D)现在称为第1代对象。第1代对象已经经历了一个垃圾回收器的检查。
图5 经历一次垃圾回收,第0代的幸存者被提升至第1代 一次垃圾回收后,第0代就不包含任何对象了。和之前一样,新对象会分配到第0代中。
图5 第0代分配了新对象;第1代又垃圾产生 假定分配新对象L会造成第0代超出预算,造成垃圾回收。CLR需要决定检查哪些代。CLR初始化时不仅会给第0代对象选择预算,第1代对象也得又预算。假如开启垃圾回收时,第1代占用内存少于预算,垃圾回收只检查第0代对象。由于忽略了第1代对象,所以加快了垃圾回收速度。
性能的优势和对应的问题
显然,忽略第1代对象能提升性能。对性能提升的部分更主要是不必变量托管堆中的每个对象。如果根或对象引用了第1代的某个对象,垃圾回收器就会忽略老对象内部的所有引用,能更短的构造好可达对象图。当然,老对象字段也有可能引用新对象。为了确保对老对象的已更新字段进行检查,垃圾回收器利用了JIT编译器内部的一个机制。这个机制在对象的引用字段发生变化时,会设置一个对应的位标志。这样,垃圾回收器就知道自上一次垃圾回收以来,哪些老对象已被写入。只有字段发生变化的老对象才需要检查是否引用了第0代中的任何新对象。
基于对象越老,生存期越长的假设。第1代对象在应用中很可能是继续可达的。因此对第1代垃圾回收很可能是浪费时间。如果有垃圾在第1代中,那就留在那里。
图6 经过两次垃圾回收后,第0代的幸存者被提升到第1代(第1代的大小增加);第0代又空出来了 图7 多次第0代回收后,第1代超出预算。两代一起进行垃圾回收 图8 第1代幸存者提升至第2代,第0代幸存者提升至第1代,第0代空出 注意</description>
</item>
<item>
<title>《CLR via C#》 异常处理</title>
<link>https://www.sooda.net.cn/posts/clrviacsharp-exception/</link>
<pubDate>Mon, 29 Aug 2022 09:39:19 +0800</pubDate>
<guid>https://www.sooda.net.cn/posts/clrviacsharp-exception/</guid>
<description>定义异常 异常是指成员没有完成它的名称所宣称的行动(方法执行中出错中断)。
异常处理机制 C#支持用try块、catch块和finally块来处理异常的各个部分。
负责会抛出异常代码的监听放到一个try块。 负责异常恢复的代码应放到一个或多个catch块。 负责清理的代码应放到一个try块。 try块 一个try块至少要有一个关联的catch块或finally块。
提示
如果一个try块中执行多个可能抛出同一个异常类型的操作,但不同的操作有不同的异常恢复措施,就应该将每个操作都放到它自己的try块中,才能正确的恢复状态。
catch块 一个try块可以关联0个或多个catch块。catch关键字后的圆括号中的表达式称为捕捉类型。C#要求捕捉类型必须是System.Exception或者它的派生类。没有圆括号则默认捕捉所有异常即System.Exception。
CLR自上而下搜索匹配的catch块,所以应该将较具体的异常放在顶部。也就是说,派生程度最大的异常放在最上边,接着是它的基类型。最后是System.Exception。
一旦CLR找到匹配的catch块,就会执行内层所有finally块中的代码。所谓“内层finally块”是指从抛出异常的try块开始,到匹配异常的catch块之间的所有finally块。(不是finally块在try块和catch块之间,而是调用函数栈中的finally块)。
1static void Func() 2{ 3 try { 4 throw new FieldAccessException(); 5 } finally { 6 Console.WriteLine(&#34;Func finally&#34;); 7 } 8} 9 10// 执行Main函数时,调用Func函数,Func函数中的try块会触发异常而 11// 捕捉的catch块在Main函数中,所以Func函数中的finally块被执行。 12// 所有内层`finally`块执行完毕后,匹配异常的`catch`块才开始执行。 13// 输出结果为: 14// Func finally 15// catch FieldAccessException 16// Main finally 17static void Main(string[] args) 18{ 19 try { 20 Func(); 21 } catch(FieldAccessException e) { 22 Console.</description>
</item>
<item>
<title>《CLR via C#》 可空值类型</title>
<link>https://www.sooda.net.cn/posts/clrviacsharp-nullable/</link>
<pubDate>Fri, 26 Aug 2022 15:58:14 +0800</pubDate>
<guid>https://www.sooda.net.cn/posts/clrviacsharp-nullable/</guid>
<description>System.Nullable&lt;T&gt;结构 可空值类型能表示null主要是因为多了一个Boolean类型的字段。其本身是值类型,仍然可存储在栈上。
1[Serializable, StructLayout(LayoutKind.Squential)] 2public struct Nullable&lt;T&gt; where T : struct { 3 private Boolean hasValue = false; // 假定null 4 internal T value = default(T); // 假定所有位都为零 5 // 其他很多方法... 6} 使用可空值类型 C#用问号表示法来声明可控值类型 1int? x = 5; 2int? y = null; 3// 等价于 4Nullable&lt;int&gt; x = 5; 5Nullable&lt;int&gt; y = null; 对于可空值类型的转换和转型 1// 从int隐式转换为Nullable&lt;int&gt; 2int? a = 5; 3 4// 从&#39;null&#39;隐式转换为Nullable&lt;int&gt; 5int? b = null; 6 7// 从Nullable&lt;int&gt;显式转换为int 8int c = (int) a; 9 10// 从int转型为Nullable&lt;double&gt; 11double?</description>
</item>
<item>
<title>About me</title>
<link>https://www.sooda.net.cn/about/</link>
<pubDate>Thu, 25 Aug 2022 11:42:00 +0000</pubDate>
<guid>https://www.sooda.net.cn/about/</guid>
<description>Github: yLong765</description>
</item>
<item>
<title>《CLR via C#》 特性</title>
<link>https://www.sooda.net.cn/posts/clrviacsharp-attribute/</link>
<pubDate>Mon, 22 Aug 2022 16:25:27 +0800</pubDate>
<guid>https://www.sooda.net.cn/posts/clrviacsharp-attribute/</guid>
<description>定制特性 自定义特性其实就是将一些附加信息与某个目标元素关联起来的方式。编译器在托管模块的元数据中生成(嵌入)这些额外的信息。
定制特性其实是一个类型的实例。定制特性类必须直接或间接从公共抽象类System.Attribute派生。
定制特性类必须有构造器才能创建实例,所以将特性应用于目标元素时,语法类似于调用类的某个实例构造器。构造器的参数称为定位参数(positional parameter),而且是强制性的,必须指定参数。而其他的公共字段或属性被称为命名参数(named parameter),这种参数是可选的。
1// 自定义特性 2public class AttrAttribute : System.Attribute { 3 public string Name; 4 public int Num; 5 public char Char; 6 public AttrAttribute(string name) { 7 Name = name; 8 } 9} 10 11// name属于定位参数 12// Num与Char属于命名参数 13[Attr(&#34;String&#34;, Num = 1, Char = &#39;c&#39;)] 14public void Func() { 15 16} 提示
将特性应用于源代码中的目标元素时,C#编译器允许省略Attribute后缀以减少打字量,并提升源代码的可读性。
CLR允许将特性应用于可在文件的元数据中表示的几乎任何东西。C#只允许将特性应用于定义以下任何目标元素的源代码:程序集、模块、类型(类、结构、枚举、接口、委托)、字段、方法(含构造器)、方法参数、方法返回值、属性、事件和泛型类型参数。应用特性时,C#允许使用一个前缀明确指定特性要应用于的目标元素。
1using System; 2[assembly: SomeAttr] // 应用于程序集 3[module: SomeAttr] // 应用于模块 4[type: SomeAttr] // 应用于类型 5internal sealed class SomeType&lt;[typevar: SomeAttr] T&gt; { // 应用于泛型类型变量 6 [field: SomeAttr] // 应用于字段 7 public int SomeField = 0; 8 [return: SomeAttr] // 应用于返回值 9 [method: SomeAttr] // 应用于方法 10 public int SomeMethod([param: SomAttr] int SomeParam) { // 应用于参数 11 return SomeParam; 12 } 13 [property: SomAttr] // 应用于属性 14 public string SomeProp { 15 [method: SomeAttr] // 应用于get访问器方法 16 get { return null; } 17 } 18 [event: SomeAttr] // 应用于事件 19 [field: SomeAttr] // 应用于编译器生成的字段 20 [method: SomeAttr] // 应用于编译器生成的add和remove方法 21 public event EventHandler SomeEvent; 22} 限制特性的应用范围 向特性应用System.</description>
</item>
<item>
<title>《CLR via C#》 委托</title>
<link>https://www.sooda.net.cn/posts/clrviacsharp-delegate/</link>
<pubDate>Thu, 18 Aug 2022 16:55:54 +0800</pubDate>
<guid>https://www.sooda.net.cn/posts/clrviacsharp-delegate/</guid>
<description>在非委托C/C++中,非成员函数的地址只是一个内存地址。这个地址不携带任何额外信息。它们很轻量但是不是类型安全的。
委托使用方式 1// 声明一个委托类型,它的实例引用一个方法 2// 该方法获取一个int参数,返回void 3public delegate void DelFunc(int value); 4 5public class DelClass { 6 public static void Func1(int value) { 7 Console.WriteLine(&#34;Func1 &#34; + value); 8 } 9 10 public static void Func2(int value) { 11 Console.WriteLine(&#34;Func2 &#34; + value); 12 } 13 14 public void Func3(int value) { 15 Console.WriteLine(&#34;Func3 &#34; + value); 16 } 17} 18 19DelClass dc = new DelClass(); 20DelFunc df1 = new DelFunc(DelClass.</description>
</item>
<item>
<title>《CLR via C#》 数组</title>
<link>https://www.sooda.net.cn/posts/clrviacsharp-array/</link>
<pubDate>Thu, 18 Aug 2022 14:09:37 +0800</pubDate>
<guid>https://www.sooda.net.cn/posts/clrviacsharp-array/</guid>
<description>CLR支持一维、多维和交错数组。所有数组都隐式地从System.Array抽象类派生,后者又派生自System.Object。这意味着数组始终是引用类型,从托管堆分配。
一维数组 1int[] arr; // 声明一个数组引用 2arr = new int[100]; // 创建含有100个int的数组,并全部初始化为0 实际上,除了数组元素,数组对象占据的内存块还包含一个类型对象指针、一个同步块索引和一些额外的成员。
注意
应尽可能的使用一维0基数组。因为其性能是最佳的,可以使用一些特殊的IL指令来处理。
多维数组 1double[,] darr = new double[10, 20]; // 二维数组 2string[,,] sarr = new string[5, 3, 10]; // 三维数组 交错数组 交错数组:数组构成的数组。0基一维交错数组的性能和普通向量一样好。不过访问交错数组的元素意味着必须进行两次或更多此访问。
1int[][] arr = new int[3][]; // 创建由多个int数组构成的一维数组 2arr[0] = new int[10]; // arr[0]引用一个含有10个int实例的数组 3arr[1] = new int[20]; // arr[1]引用一个含有20个int实例的数组 4arr[2] = new int[30]; // arr[2]引用一个含有30个int实例的数组 数组初始化 1// 大括号中以逗号分隔的数据项称为数组初始化器 2String[] names = new String[] {&#34;A&#34;, &#34;B&#34;}; 3// 隐式类型数组。 4var kids = new[] {new {Name=&#34;A&#34;}, new {Name=&#34;B&#34;} }; 数组转型 CLR不允许将值类型元素的数组转型为其它任何类型。(不过可用Array.</description>
</item>
<item>
<title>《CLR via C#》 枚举和标志位</title>
<link>https://www.sooda.net.cn/posts/clrviacsharp-enum/</link>
<pubDate>Thu, 18 Aug 2022 10:23:40 +0800</pubDate>
<guid>https://www.sooda.net.cn/posts/clrviacsharp-enum/</guid>
<description>枚举类型 枚举类型定义了一组“符号名称/值”的配对。且是强类型的。
1enum Color { 2 White, // 赋值0 3 Red, // 赋值1 4 Green, // 赋值2 5} 每个枚举类型都是从System.Enum派生,后者从System.ValueType派生,而System.ValueType从System.Object派生。所以枚举是值类型,但枚举不能定义任何方法、属性或事件。不过可以利用扩展方法模拟向枚举类型添加方法。
1public static class EnumExtension { 2 public static bool Extension(this Color color) { 3 // ... 4 } 5} 编译器编译枚举类型时,把每个符号转换成类型的一个常量字段。类似于如下代码
1// 伪代码 2public struct Color : System.Enum { 3 public const Color White = (Color) 0; 4 public const Color Red = (Color) 1; 5 public const Color Green = (Color) 2; 6 // 以下是一个公共实例字段,包含Color变量的值,不能写代码来直接引用该字段 7 public int value__; 8} 注意</description>
</item>
<item>
<title>《CLR via C#》 字符、字符串和文本处理</title>
<link>https://www.sooda.net.cn/posts/clrviacsharp-string/</link>
<pubDate>Tue, 16 Aug 2022 15:57:16 +0800</pubDate>
<guid>https://www.sooda.net.cn/posts/clrviacsharp-string/</guid>
<description>字符 字符总是表示成16位Unicode代码值,每个字符都是System.Char接口(值类型)的实例。
Sytem.Char 方法 方法名 说明 GetUnicodeCategory 返回UnicodeCategory枚举,表明该字符是由Unicode标准定义的控制字符、货币符号、小写字母、大写字母、标点符号还是其他字符。 ToLowerInvariant 忽略语言文化将字符转换位小写 ToLower 转换小写,但会使用与调用线程关联的语言文化信息(System.Threading.Thread的静态属性CurrentCulture) Equals 两个Char实例代表同一个16位Unicode码位的前提下返回true CompareTo 两个Char实例忽略语言文化的比较结果 GetNumericValue 返回字符的数值形式 Char实例的相互转换 转型(强制类型转换):是效率最高的,因为编译器会生成中间语言指令来执行转换,而且不必调用方法。 使用Convert类:System.Convert提供了几个静态方法来实现Char和数值类型的转换。所有转换都以checked方式执行,发现转换将造成数据丢失就会抛出OverflowException异常。 使用IConvertible接口:效率最差,因为再值类型上调用接口方法会装箱。而且很多是显式实现的接口。 System.String 字符串构造 C#将String视为基元类型,也就是允许再源代码中直接使用字面值(literal)字符串。编译器将这些字符串放到模块的元数据中,并在运行时加载和引用它们。
逐字字符串(verbatim string):用@来声明字符串,其字符串中所有的字符都视为一部分。下面的代码在元数据中生成同样的字符串,但是逐字字符串可读性更好。
1string file = &#34;C:\\Windows\\System32\\Notepad.exe&#34;; 2string file = @&#34;C:\Windows\System32\Notepad.exe&#34;; 字符串是不可变的 字符串一经创建便不能更改。使字符串不可变有几点好处
允许对字符串进行操作,但不实际更改字符串。 在操纵或访问字符串时不会发生线程同步问题。 可通过一个String对象共享多个一致的String内容,从而节省内存——字符串留用(string interning)。 比较字符串 判断字符串相等性方法:
Equals Compare StartsWith EndsWith 出于变成目的比较字符串时,应该用StringComparsion.Ordinal或者StringComparison.OrdinalIgnoreCase。忽略语言文化比较最快。
要以语言文化来比较字符串,就应该用StringComparsion.CurrentCulture或者StringComparison.CurrentCultureIgnoreCase。
注意
应该用String.ToUpperInvariant来进行正规化(normalizing)。因为编译器对执行大写比较的代码有优化
注意
CompareTo默认执行对语言文化敏感的比较,而Equals执行普通的序号比较
语言文化的字符串比较 System.Globalization.CultureInfo类表示一个“语言/国家”对。在CLR中每个线程都关联了两个特殊属性,每个属性都引用一个CultureInfo对象。
CurrentUICulture属性:获取要向用户显式的资源。多用于GUI或Web窗体中。 CurrentCulture属性:不适合CurrentUICulture属性的场合就使用该属性。 字符串留用 CLR初始化时会创建一个内部哈希表。键是字符串,而值是对托管堆中String对象的引用。
String类提供了两个方法,便于你访问这个哈希表
Intern方法获取一个String,获得它的哈希码。并在内部检查是否有匹配。有就返回当前引用,无则创建字符串副本,并将副本添加到哈希表中,返回对其的引用。 IsIntern方法获取一个String,获得它的哈希码。如果哈希表中没有匹配则返回null。 字符串池 编译器只在模块的元数据中只将字面值字符串写入一次。引用该字符串的所有代码都被修改成引用元数据中同一个字符串。编译器也将当个字符串的多个实例合并成一个实例,显著减少模块大小。
利用StringBuilder高效率构造字符串 StringBuilder对象包含一个字段,该字段引用了由Char结构构成的数组。可利用StringBuilder的成员来高效率的操纵该数组。如果字符串变大,超过了预先分配的大小,则会自动分配一个新的、更大的数组,复制字符,并开始使用新数组,前一个数组会被垃圾回收。
ToString方法 System.Object定义了一个public、virtual、无参的ToString方法。这个方法代表对象当前值的字符串,而且应该根据语言文化进行格式化。
调试器中变量上方出现的数据提示的文本就是通过调用对象的ToString方法来获取的。
编码 UTF-16(Unicode):将每16位字符编码成2个字节。不对字符产生影响,也不发生压缩————性能出色。 UTF-8:值在0x0080之下的字符压缩成1字节,0x0080~0x07FF转换成2字节。0x0800以上位4字节。代理项对表示成4字节。0x0800以上效率不如UTF-16。 UTF-32:使用4字节来编码所有字符。 ASCII:将16位字符编码范围只有0x00~0x7F </description>
</item>
<item>
<title>《CLR via C#》 接口</title>
<link>https://www.sooda.net.cn/posts/clrviacsharp-interface/</link>
<pubDate>Mon, 08 Aug 2022 17:28:55 +0800</pubDate>
<guid>https://www.sooda.net.cn/posts/clrviacsharp-interface/</guid>
<description>接口实际只是对一组方法签名进行了统一命名。这些方法不提供任何实现。类通过指定接口名称来继承接口,而且必须显式实现接口方法,否则CLR会认为此类型定义无效。
定义接口 接口可以定义事件、无参属性和有参属性。但接口不能定义任何构造器方法,也布恩那个定义任何实例字段。
提示
CLR允许接口定义静态方法、静态字段、常量和静态构造器,但C#不允许。
注意
对于CLR来说接口定义就是类型定义。
继承接口 C#编译器要求将实现接口的方法(接口方法)标记为public。CLR要求将接口方法标记为virtual。不将方法标记为virtual,编译器会将他们标记为virtual和sealed;这会阻止派生类重写接口方法。将方法显式标记为virtual,编译器就会将该方法标记为virtual(并保持他的非密封状态),使派生类能重写它。
派生类不能重写sealed接口方法。但派生类可以重新继承同一个接口,并为接口方法提供自己的实现。在对象上调用接口方法时,调用的是该方法在该对象的类型中的实现。
1// 这个类派生自Object,它实现了IDisposable 2internal class Base : IDisposable { 3 // 这个方法隐式密封,不能被重写 4 public void Dispose() { 5 Console.WriteLine(&#34;Base&#39;s Dispose&#34;); 6 } 7} 8 9// 这个类派生自Base,它重新实现了IDisposable 10internal class Derived : Base, IDisposable { 11 // 这个方法不能重写Base的Dispose,&#39;new&#39;表明该方法重新实现了IDisposable的Dispose方法 12 new public void Dispose() { 13 Console.WriteLine(&#34;Derived&#39;s Dispose&#34;); 14 // 调用基类的实现 15 base.Dispose(); 16 } 17} 18 19Base b = new Base(); 20b.Dispose(); // 显示 &#34;Base&#39;s Dispose&#34; 21((IDisposable)b).</description>
</item>
<item>
<title>《CLR via C#》 泛型</title>
<link>https://www.sooda.net.cn/posts/clrviacsharp-%E6%B3%9B%E5%9E%8B/</link>
<pubDate>Sun, 07 Aug 2022 15:38:51 +0800</pubDate>
<guid>https://www.sooda.net.cn/posts/clrviacsharp-%E6%B3%9B%E5%9E%8B/</guid>
<description>泛型(generic)是CLR和变成语言提供的一种特殊机制,它支持另一种形式的代码重用,即“算法重用”。
CLR允许创建泛型引用类型和泛型值类型,但不允许创建泛型枚举类型。CLR还允许创建泛型接口和泛型委托。
类型实参(type argument):使用泛型类型或方法时指定的具体数据类型。
泛型的优势:
源代码保护:开发人员不需访问算法的源代码,而C++模板则必须要将源码提供给开发人员。 类型安全:保证只有与指定数据兼容的对象才能用于算法。 更清晰的代码:减少了源代码中必须进行强制类型转换次数,使代码容易编写和维护。 更佳的性能:值类型的实例能以传值方式传递,不需要装箱。由于不需要进行强制类型转换,所以CLR无需验证转型是否正确,提高效率。 提示
运算性能计时代码
1sealed class OperationTimer : IDisposable { 2 private Stopwatch _stopwatch; 3 private String _text; 4 private int _collectionCount; 5 6 public OperationTimer(String text) { 7 PrepareForOperation(); 8 _text = text; 9 _collectionCount = GC.CollectionCount(0); 10 // 此语句应是方法中的最后一句,从而保证计时准确性 11 _stopwatch = Stopwatch.StartNew(); 12 } 13 14 public void Dispose() { 15 Console.WriteLine(&#34;{0} (GCs={1,3}) {2}&#34;, _stopwatch.Elapsed, GC.CollectionCount(0) - _collectionCount, _text); 16 } 17 18 private static void PrepareForOperation() { 19 GC.</description>
</item>
<item>
<title>《CLR via C#》 Class成员</title>
<link>https://www.sooda.net.cn/posts/clrviacsharp-class%E6%88%90%E5%91%98/</link>
<pubDate>Wed, 27 Jul 2022 15:20:36 +0800</pubDate>
<guid>https://www.sooda.net.cn/posts/clrviacsharp-class%E6%88%90%E5%91%98/</guid>
<description>类型成员基础 常量:数据恒定不变的符号。 字段:只读或可读/可写的数据值。 实例构造器:将新对象的实例字段初始化的特殊方法 类型构造器:将类型的静态字段初始化的特殊方法。 方法:更改或查询类型或对象状态的函数。作用于类型成为静态方法,作用于对象为实例方法。 操作符重载:实际是方法,定义了当操作符作用于对象时,应该如何操作对象。 转换操作符:定义如何隐式或显式将对象从一种类型转型为另一种类型的方法。 属性:设置或查询类型或对象的逻辑状态。作用于类型为静态属性,作用域对象为实例属性。 事件:静态事件允许类型向一个或多个静态或实例方法发送通知。实例事件允许对象向一个或多个静态或实例方法发送通知。 类型:可以定义其他嵌套类型。 常量 定义常量符号时,值必须能在编译时确定。编译器将常量值保存到程序集元数据中。意味着只能定义编译器可识别的基元类型常量。非基元类型的常量变量只能为null。
编译器会在定义常量的程序集元数据中查找该符号,提取常量值并嵌入IL代码中。所以运行时没有任何额外内存分配,但是不能取常量地址或引用传递常量。
C#不允许常量设置为static,因为常量总隐式设置为static。
提示
如果希望在运行时从一个程序集中提取另一个程序集中的值,那么不应该使用常量,而应该使用readonly字段。
字段 字段是一种数据成员,容纳了一个值类型的实例或对一个引用类型的引用。
CLR支持类型(静态)字段和实例(非静态)字段。类型字段所需的动态内存是在类型对象中分配的,而类型对象是在类型加载到一个AppDomain时创建的。类型加载到AppDmain的时机又是在引用该类型的任何方法首次进行JIT编译的时候。实例字段容纳字段数据所需的动态内存是在构造类型的实例时分配的。
字段修饰符 CLR术语 C#术语 说明 Static static 类型(静态)字段 Instance (默认) 实例(非静态)字段 InitOnly readonly 只能有一个构造方法中的代码写入(但可利用反射修改readonly字段) Volatile volatile 编译器、CLR和硬件不会对访问这种字段的代码执行“线程不安全”的优化措施 提示
当某个字段时引用类型,并且该字段呗标记为readonly时,不可改变的是引用,而非字段引用的对象。
方法 实例构造器(引用类型) 实例构造器是初始化类型的实例的特殊方法。构造器方法在“方法定义元数据表”中始终叫做.ctor。创建引用类型的实例时,首先为实例的数据字段分配内存,然后把内存部分归零(保证没有被构造器显式赋值的所有字段都为0或null值),接着初始化对象的附加字段(类型对象指针和同步索引块),最后调用类型的实例构造器来设置对象的初始状态。
注意事项 实例构造器永远不能被继承。所以不能使用:virtual,new,override,sealed,abstract修饰符。 如果没有显式定义构造器,C#编译器则会默认定义一个(无参)构造器。这个构造器只是简单的调用了基类的构造器 1public class A {} 2//等价于: 3public class A { 4 public A() : base() {} 5} 如果类的修饰符为abstract,则编译器生成的默认构造器的可访问性就为protected。否则为public。 如果类的修饰符为static,则编译器不会生成默认构造器。 为了使代码“可验证”,类的实例构造器在访问从基类继承的任何字段之前,必须先调动基类的构造器。如果派生类的构造器没有显式调用一个基类构造器,C#编译器会自动生成对默认的基类构造器的调用。 极少数可以在不调用实例构造器的前提下创建类型的实例。比如Object的MemberwiseClone方法、或反序列化时。 不要在构造函数中调用虚方法。 C#编译器以“内联”(嵌入)方式初始化实例字段。在每个构造器方法开始的位置,包含类字段中直接赋值的代码。在其后会插入对基类构造器的调用。然后再插入构造函数的代码。
1class B { } 2class A { 3 public int a = 1; 4 public int b = 3; 5 public A() { 6 b = 2; 7 } 8} 9 10// C#编译器编译后的代码为 11class A : B { 12 public int a; 13 public int b; 14 public A() { 15 a = 1; 16 b = 3; 17 // 调用父类构造器 18 b = 2; // b会被重复赋值 19 } 20} 提示</description>
</item>
<item>
<title>《CLR via C#》 基元类型、引用类型和值类型</title>
<link>https://www.sooda.net.cn/posts/clrviacsharp-%E5%9F%BA%E5%85%83%E5%BC%95%E7%94%A8%E5%80%BC%E7%B1%BB%E5%9E%8B/</link>
<pubDate>Tue, 12 Jul 2022 17:49:41 +0800</pubDate>
<guid>https://www.sooda.net.cn/posts/clrviacsharp-%E5%9F%BA%E5%85%83%E5%BC%95%E7%94%A8%E5%80%BC%E7%B1%BB%E5%9E%8B/</guid>
<description>Type基础 C#中有三种类型分别是基元类型、引用类型和值类型
基元类型 编辑器直接支持的数据类型称为基元类型(primitive type)。基元类型直接映射到Framework类库(FCL)中存在的类型。
C#基元类型 FCL类型 符合公共语言规范(CLS) 说明 sbyte System.SByte 否 有符号8位 byte System.Byte 是 无符号8位 short System.Int16 是 有符号16位 ushort System.UInt16 否 无符号16位 int System.Int32 是 有符号32位 uint System.UInt32 否 无符号32位 long System.Int64 是 有符号64位 ulong System.UInt64 否 无符号64位 char System.Char 是 16位Unicode字符 float System.Single 是 IEEE 32位浮点值 double System.Double 是 IEEE 64位浮点值 bool System.Boolean 是 true/false decimal System.Decimal 是 128位高精度浮点值,常用于不容许误差的金融计算 string System.String 是 字符数组 object System.Object 是 所有类型的基类 dynamic System.</description>
</item>
<item>
<title>《CLR via C#》 class及成员基础</title>
<link>https://www.sooda.net.cn/posts/clrviacsharp-class/</link>
<pubDate>Tue, 28 Jun 2022 17:09:01 +0800</pubDate>
<guid>https://www.sooda.net.cn/posts/clrviacsharp-class/</guid>
<description>class System.Object “运行时”所有的类型最终都从System.Object类型派生。
1// 隐式派生自Object 2class A {} 3 4// 显式派生自Object 5class A : System.Object {} System.Object的public方法
Equals:比较函数 GetHashCode:对象哈希值 ToString:转换成String类型对象 GetType:返回对象的类型 System.Object的protected方法
MemberwiseClone:创建类型的新实例,并将新对象的实例字段设置与this对象的实例字段完全一致。返回对新实例的引用 Finalize:垃圾回收阶段实际清理之前会调用此方法。 new操作符都做了什么工作 1A a = new A(&#34;123&#34;); 上面的代码中new操作符所作的事情:
计算类型机器所有基类型(一直到System.Object)中定义的所有实例字段所需要的字节数。并加上每个对象都需要额外开销的类型对象指针(type object pointer)和同步块索引(sync block index) 。 从托管堆中分配1步骤中所计算的类型的字节数,从而分配对象的内存,分配的所有字节都设置为零(0)。 初始化对象的类型对象指针和同步块索引成员。 调用类型的实例构造器,传递在new调用中指定的实参。编译器会在构造函数(.ctor)中自动生成代码调用基类构造器 。最终都会调用System.Object的构造函数,但它什么都不做,单纯的返回。 返回指向新建对象的一个引用(或指针) 类型转换 CLR允许将对象转换为它的(实际)类型或者它的任何基类型。然而,将对象转换为派生类时,因可能转换失败,所以要求显示转换。
is 和 as is操作符用于检查是否兼容于指定类型,如果对象引用null则总是返回false
1Object o = new Object(); 2o is Object; // 返回true 3o is A; // 返回false as操作符用于检查是否兼容于指定类型,并进行转换。如果兼容则返回对于类型,不兼容则返回null。(判断null比检查是否兼容于指定类型快)
1A a = o as A; 2if (a !</description>
</item>
<item>
<title>《CLR via C#》 CLR基础</title>
<link>https://www.sooda.net.cn/posts/clrviacsharp-clr%E5%9F%BA%E7%A1%80/</link>
<pubDate>Thu, 16 Jun 2022 11:16:04 +0800</pubDate>
<guid>https://www.sooda.net.cn/posts/clrviacsharp-clr%E5%9F%BA%E7%A1%80/</guid>
<description>CLR的执行模型 公共语言运行时(Common Language Runtime, CLR)是一个可由多种编程语言使用的“运行时”。CLR的核心功能(比如内存管理、程序集加载、安全性、异常处理和线程同步)可由面向CLR的所有语言使用。
不同编程语言的意义?可将编译器视为语法检查器和“正确代码”分析器。它们检查代码,确定写的一切都有意义,并输出对其意图进行描述的代码。但是无论任何用哪个编译器,最终编译的结果都是托管模块(managed module)。托管模块是标准的32位Microsoft Windows可移植执行(Portable Executable, PE32)或64位(PE32+)文件。
高级语言(例如C#、F#等)通常只公开的CLR全部功能的一个子集。然而,IL汇编语言允许开发人员访问CLR的全部功能。
托管模块的组成部分 PE32或PE32+头部:标准Windows PE文件头,类似于“公共对象文件格式”(Common Oject File Format, COFF)。 CLR头部:包含使这个模块成为托管模块的信息(可由CLR和一些实用程序进行解释)。包含要求的CLR版本,一些标志(flag),托管模块入口方法(Main方法)的MethodDef元数据token以及模块的元数据、资源、强名称、一些标志及其他不太重要的数据项的位置/大小。 元数据:每个托管模块都包含元数据表。主要由两种表:一种描述源代码中定义的类型和成员,另一种描述源代码引用的类型和成员。 用途: 避免编译时对原生C/C++头和库文件的需求,因为在实现类型或成员的IL代码文件中,已包含有关引用类型或成员的全部信息。编译器直接从托管模块读取元数据。 “智能感知”(IntelliSense)技术会解析元数据,告诉你一个类型提供了哪些方法、属性、事件和字段。对于方法,还能告诉其参数。 CLR的代码验证过程使用元数据确保代码只执行“类型安全”的操作。 允许将对象的字段序列化和反序列化。 允许垃圾回收器跟踪对象生存期。 IL(中间语言)代码:编译器编译源代码时生成的代码。在运行时,CLR用本机代码编译器(native code compolers)将IL编译成面向本机特定CPU架构的指令代码。 IL指令是一种基于栈的指令集(跟Lua一样)。操作压入栈,并从让结果从栈弹出。 IL指令还是无类型(typeless)的。 将托管模块组合成程序集 程序集(assembly) 是一个或多个模块或资源文件的逻辑性分组。程序集是重用、安全性以及版本控制的最小单元。CLR实际与程序集一起工作,程序集相当于它的“组件”。
清单(manifest) 是元数据表的集合。这些表描述构成程序集的文件、程序集中的文件所实现的public类型以及与程序集关联的资源或数据文件。
程序集的自描述(self-describing) 利用程序集模块中包含的引用程序集有关的信息(版本号),CLR能判断为了执行程序集中的代码,程序集的直接依赖对象(immediate dependency)。
托管程序集总是利用Windows的数据执行保护(Data Execution Prevention, DEP)和地址空间布局随机化(Address Space Layout Randomization, ASLR)来增强系统安全性。
如果只有一个托管模块而且无资源(或数据)生成的程序集就是托管模块本身,生成无需额步骤。但如果希望将一组文件合并到程序集中,就必须用AL(程序集连接器)等工具生成。
程序集的执行 分析以下程序的执行
1static void Main() { 2 Console.WriteLine(&#34;Hello World!&#34;); 3 Console.WriteLine(&#34;Goodbye!&#34;); 4} 在Main方法执行之前,CLR会检测出Main方法的代码引用的所有类型(本例为Console)。并分配一个内部数据结构来管理对引用类型的访问。内部数据结构中对类型定义的每个方法都有一个对应的记录项。每个记录项都含有一个地址,根据此地址即可找到方法的实现。对这个结构初始化时,CLR将每个记录项的指针都指向包含在CLR内部的一个未编档函数(本书定义为JITCompiler)。 第一次执行Console.WriteLine方法时,JITCompiler函数会被调用。它负责将方法的IL代码编译成本机CPU指令。由于IL是“即时”(just in time)编译的,所有通常将CLR的这个组件叫做JIT编译器。JITCompiler函数的执行方式如下: 在负责实现类型(Console)的程序集的元数据中查找被调用的方法(WriteLine)。 从元数据中获取该方法的IL代码。并验证 动态分配内存块。 将IL代码编译成本机CPU指令然后将本机代码存储到步骤3分配的内存中。 在Type表(Main函数指向Console.WriteLine的记录项)中修改与方法对应的条目,使它指向步骤3分配的内存块。 跳转到内存块中的本机代码,并执行。 执行完后返回到Main方法中继续执行。 当第二次执行Console.</description>
</item>
<item>
<title>《游戏编程算法与技巧》人工智能篇</title>
<link>https://www.sooda.net.cn/posts/%E6%B8%B8%E6%88%8F%E7%BC%96%E7%A8%8B%E7%AE%97%E6%B3%95%E4%B8%8E%E6%8A%80%E5%B7%A7%E4%BA%BA%E5%B7%A5%E6%99%BA%E8%83%BD%E7%AF%87/</link>
<pubDate>Wed, 19 Jan 2022 21:58:45 +0800</pubDate>
<guid>https://www.sooda.net.cn/posts/%E6%B8%B8%E6%88%8F%E7%BC%96%E7%A8%8B%E7%AE%97%E6%B3%95%E4%B8%8E%E6%8A%80%E5%B7%A7%E4%BA%BA%E5%B7%A5%E6%99%BA%E8%83%BD%E7%AF%87/</guid>
<description>搜索空间的表示 最简单的寻路设计就是将图作为数据结构。图中包含多个节点,连接相邻的点组成边。图有多种表示方式,最简单的是邻接表
游戏世界用图表示的方式 游戏世界用图来表示,有多种方法,一种简单的方法就是将世界区分为一个个正方形的格子(或者六边形)。如在《文明》策略游戏中的沙盘地图。而在不规则的地图中要么使用路点要么使用导航网格。
寻路节点 光卡设计师在游戏世界中拜访AI可以到达的位置。这些点被解释为图中的节点(可手动和自动生成)
缺点:AI只能在节点和边缘的位置移动。会有很多不可移动的位置,AI显得比较生硬
导航网格 图上的节点实际上就是凸多边形。邻近节点就是简单的任意邻近凸多边形。意味着游戏世界区域可以通过少量的凸多边形表示。
凸多边形内部任意位置都是可走的。意味着AI有了大量的空间可以行动。可返回更自然的路径
启发式算法 游戏寻路的启发式算法有很多,启发式用公式$h(x)$表示,理想情况下启发式结果越接近真实越好。如果它的估算总是保证小于等于真实开销,那么这个启发式是可接受的。目前比较流行的启发式算法就是A*算法。不过还有贪婪最佳优先算法,Dijkstra算法等。这里只介绍A*算法
A*算法 除了启发式 $h(x)$ 需要考虑,A又考虑了路径开销$g(x)$,所以A的节点开销等式为: $$ f(x) = g(x) + h(x) $$
在当前节点搜索可到达的节点加入开放集合中,并从中选出开销最小的点放入关闭集合,如此往复。而$g(x)$的开销取决于父节点的$g(x)$的开销。这意味着父节点是可选的,$g(x)$的开销是可变得</description>
</item>
<item>
<title>《游戏编程算法与技巧》摄像机篇</title>
<link>https://www.sooda.net.cn/posts/%E6%B8%B8%E6%88%8F%E7%BC%96%E7%A8%8B%E7%AE%97%E6%B3%95%E4%B8%8E%E6%8A%80%E5%B7%A7%E6%91%84%E5%83%8F%E6%9C%BA%E7%AF%87/</link>
<pubDate>Fri, 14 Jan 2022 21:58:45 +0800</pubDate>
<guid>https://www.sooda.net.cn/posts/%E6%B8%B8%E6%88%8F%E7%BC%96%E7%A8%8B%E7%AE%97%E6%B3%95%E4%B8%8E%E6%8A%80%E5%B7%A7%E6%91%84%E5%83%8F%E6%9C%BA%E7%AF%87/</guid>
<description>摄像机的基础属性 视场 观看世界的角度和广度,称为视场(FOV)。人双目并视大约能看到120°的视场,而剩余的视场在边缘能够发现运动,但不清晰
如果视场变的太大就会有鱼眼效果,屏幕边缘会弯曲,类似于广角镜头
高宽比 视口的宽度和高度的比率(如4:3、16:9等)
摄像机的跟随实现 基础跟随 跟随在某个对象后方,保持固定距离,固定的俯视角摄像机
1// tPos: 目标位置 2// tForward: 目标方向 3// hDist: 水平跟随距离 4// vDist: 垂直跟随距离 5void BasucFollowCamera(GameObject target, float hDist, float vDist) { 6 // 计算相机位置 7 Vector3 pos = target.position - target.forward * hDist + target.up * vDist; 8 // 计算相机朝向 9 Vector3 cameraForward = tPos - pos; 10 // 设置相机位置和朝向 11 //... 12} 弹性跟随相机 实现相机到相机目标位置的逐渐变化,可以理解为真实位置的相机和在基础跟随所要到达的理想位置中间有一个弹簧
1class SpringCamera { 2 float hDist, vDist; // 水平和垂直跟随距离 3 float springConstant; // 弹性常量:越高弹性越小 4 float dampConstant; // 阻尼常量:由弹性常量决定 5 Vector3 velocity, actualPosition; // 速度和摄像机真实位置 6 GameObject target; // 跟随目标 7 8 void init(GameObject _target, float _springConstant, float _hDist, float vDist) { 9 target = _target; 10 springConstant = _springConstant; 11 hDist = _hDist; 12 vDist = _vDist; 13 // 计算阻尼常量 14 dampConstant = 2.</description>
</item>
<item>
<title>《游戏编程算法与技巧》物理篇</title>
<link>https://www.sooda.net.cn/posts/%E6%B8%B8%E6%88%8F%E7%BC%96%E7%A8%8B%E7%AE%97%E6%B3%95%E4%B8%8E%E6%8A%80%E5%B7%A7%E7%89%A9%E7%90%86%E7%AF%87/</link>
<pubDate>Tue, 04 Jan 2022 21:58:45 +0800</pubDate>
<guid>https://www.sooda.net.cn/posts/%E6%B8%B8%E6%88%8F%E7%BC%96%E7%A8%8B%E7%AE%97%E6%B3%95%E4%B8%8E%E6%8A%80%E5%B7%A7%E7%89%A9%E7%90%86%E7%AF%87/</guid>
<description>平面 平面在游戏中倾向的数学定义:
$$ P \cdot \hat{n} + d = 0 $$
$P$是平面上任意一点,$\hat{n}$是平面法线,$d$是平面到原点的最小距离
1// 平面的数据结构 2struct Plane { 3 Vector3 normal; 4 float d; 5} 射线和线段 游戏中射线基本就是线段,因为基本不会让射线无限延伸下去。数学定义为:
$$ R(t) = R_0 + \vec{v}t $$
$R_0$是起点,$\vec{v}$是射线方向,$t$必须大于等于0。而在代码中的数据结构我们存两个点一个起点(startPoint),一个终点(endPoint)。则$R_0$就是startPoint,$\vec{v}$就是endPoint-startPoint,而$t$的取值就为0~1了
1struct RayCast { 2 Vector3 startPoint 3 Vector3 endPoint 4} 碰撞几何体 包围球 通过中心点和半径表示
1class ShpereCollider { 2 Vector3 center; 3 float radius; 4} 轴对齐包围盒(AABB) 每条边都与x轴或y轴平行的矩形,只能随轴旋转,多用于2D
1class AABB2D { 2 Vector2 min 3 Vector2 max 4} 朝向包围盒OBB 可以自由旋转的类AABB包围盒,较复杂</description>
</item>
<item>
<title>《游戏编程算法与技巧》输入和声音篇</title>
<link>https://www.sooda.net.cn/posts/%E6%B8%B8%E6%88%8F%E7%BC%96%E7%A8%8B%E7%AE%97%E6%B3%95%E4%B8%8E%E6%8A%80%E5%B7%A7%E8%BE%93%E5%85%A5%E5%92%8C%E5%A3%B0%E9%9F%B3%E7%AF%87/</link>
<pubDate>Fri, 24 Dec 2021 08:55:57 +0800</pubDate>
<guid>https://www.sooda.net.cn/posts/%E6%B8%B8%E6%88%8F%E7%BC%96%E7%A8%8B%E7%AE%97%E6%B3%95%E4%B8%8E%E6%8A%80%E5%B7%A7%E8%BE%93%E5%85%A5%E5%92%8C%E5%A3%B0%E9%9F%B3%E7%AF%87/</guid>
<description>游戏输入 可分为两种:数字与模拟
数字输入 只用两种状态:“按下”和“没有按”(例如键盘)
如何实现一直按着的判断? 同时跟踪上一帧和这一帧的状态,根据这两个状态来判断
上一帧状态 本帧状态 结论 释放 释放 一直释放 释放 按下 刚刚按下 按下 释放 刚刚释放 按下 按下 一直按下 伪代码:
1enum KeyState { 2 StillReleased, 3 JustPressed, 4 JustReleased, 5 StillPressed 6} 7 8lastState[256]; // 上一帧状态 9currentState[256]; // 当前帧状态 10 11// 每帧更新状态 12void UpdateKeyboard() { 13 lastState = currentState; 14 currentState = GetKeyboardState(); // 获取当前状态 15} 16 17// 通过keyCode获取KeyState 18KeyState GetKeyState(int keyCode) { 19 if (lastState[keyCode]) 20 if (currentState[keyCode]) 21 return StillReleased; 22 else 23 return JustReleased; 24 else 25 if (currentState[keyCode]) 26 return JustPressed; 27 else 28 return StillReleased; 29} 模拟输入 可返回某个数字的范围(如遥感)。但遥感的数值基本不会归零所以需要输入过滤,来消除偏差值。一般取遥感总值的10%为无效值。</description>
</item>
<item>
<title>《游戏编程算法与技巧》渲染篇</title>
<link>https://www.sooda.net.cn/posts/%E6%B8%B8%E6%88%8F%E7%BC%96%E7%A8%8B%E7%AE%97%E6%B3%95%E4%B8%8E%E6%8A%80%E5%B7%A7%E6%B8%B2%E6%9F%93%E7%AF%87/</link>
<pubDate>Wed, 22 Dec 2021 15:57:24 +0800</pubDate>
<guid>https://www.sooda.net.cn/posts/%E6%B8%B8%E6%88%8F%E7%BC%96%E7%A8%8B%E7%AE%97%E6%B3%95%E4%B8%8E%E6%8A%80%E5%B7%A7%E6%B8%B2%E6%9F%93%E7%AF%87/</guid>
<description>闲话 近日读完了《游戏编程算法与技巧》这本书。更感觉是《游戏引擎架构》的缩略图。先阅读此书会对游戏开发有一个整体的认识与理解。适合需要了解游戏开发的初学者阅读
此书对于我也是梳理游戏开发知识点的概括好书。对于此书会在重点部分做提炼。
渲染基础 此部分只做简单的总结,网上已经有很多的好文章了。而此书作者也没有很详细的写这部分。毕竟这块要是讲细了够学一辈子了
推荐一些好文(有兴趣可以看看我写的软渲染)
闫令琪老师的GAMES101课程 韦易笑老师的mini3d项目 韦易笑老师的RenderHelp项目 Dmitry V. Sokolov老师的tinyrender项目 双缓冲技术解决渲染撕裂 CRT显示器时代,所谓场消隐期(VBLANK):喷枪从右下角移动到左上角所花费的时间
渲染撕裂:显示器在绘制像素缓冲区中的内容时,游戏输出了新的渲染结果到像素缓冲区,导致渲染撕裂
双缓冲技术:解决屏幕撕裂,用两块像素缓冲区,游戏交替的在这两块缓冲区中绘制
垂直同步:让交换缓冲区的时机在场消隐期进行
三缓冲技术:利用3个缓冲区,让画面更加平滑,但增加输入延迟
画家算法 所有物体按照从后往前的顺序绘制。优点:绘制绝对正确(包括透明物体),缺点:overdraw高,效率慢
3D渲染 为什么用三角形表示面片?
仅用3顶点表示的最简单的多边形 三角形总在一个面上 任何3D对象都可以简单地用细分三角面表示 网格:多个三角面组成
软件光栅化 将3D模型正确渲染到2D颜色缓冲的算法
3D模型经过4个主要坐标系空间转到最终的2D颜色缓冲中
模型(局部)坐标系:相对于模型自身的坐标系(角色模型一般为两脚中间) 世界坐标系:所有对象都相对于世界原点偏移 视角(摄像机)坐标系:将世界坐标系的模型变换到相对于摄像机的位置上 投影坐标系:将3D场景平铺到2D平面上得到的坐标系 除了上面的坐标系外,还有一个特殊的坐标系,就是齐次坐标系
将4D坐标系应用在3D空间中,就被称为齐次坐标系。而第四个分量为w分量。如果w=0,则此齐次坐标是3D向量。而w=1,则表示此齐次坐标是3D的点
矩阵变换 将模型在各个坐标系中转换所用的矩阵,就是矩阵变换。
此处和之后所用的都为行向量。(OpenGL为列向量) 模型转世界坐标系 平移 将顶点移动一段距离,只作用在点上
$$ T(t_x,t_y,t_z) = \begin{bmatrix} 1 &amp; 0 &amp; 0 &amp; 0 \newline 0 &amp; 1 &amp; 0 &amp; 0 \newline 0 &amp; 0 &amp; 1 &amp; 0 \newline t_x &amp; t_y &amp; t_z &amp; 1 \end{bmatrix} $$</description>
</item>
<item>
<title>《Unity实战》总结</title>
<link>https://www.sooda.net.cn/posts/unity%E5%AE%9E%E6%88%98/</link>
<pubDate>Sun, 07 Nov 2021 22:05:19 +0800</pubDate>
<guid>https://www.sooda.net.cn/posts/unity%E5%AE%9E%E6%88%98/</guid>
<description>闲话 本文意加强对Unity的一些知识点的补充,所以基本以记录为主。
零散知识 Unity使用左手坐标系 所有场景对象都是类GameObject的实例。GameObject只是组件的容器 变量赋值数字后的f让计算机把这个变量作为浮点值处理,否则C#会把小数当成双精度值(C++也是一样) 本地坐标:对象有自己的原点,有三个轴,且坐标系跟着对象移动 全局(世界)坐标:与本地坐标的区别就是不移动 Cursor.lookState = CursorLockMode.Locked; // 光标锁定屏幕中心 Cursor.visible = false; // 隐藏光标 Unity的场景拥有所有此场景所有GameObject的引用,用Destroy来告诉引擎,将这个对象从Scene中移除 Shader中的Albedo是基础颜色的意思 用2D开启的新项目,Unity主要调整了2D Editor模式(将导入的图片设置为Sprite)和2D Scene视图 Unity中的导入设置Pixels-To-Units是像素与Unity原默认3D的一个单位的比例,默认是100:1(建议使用默认值,在1:1的状态下物理引擎不能正常工作) 像素完美(Pixel-perfect):屏幕上的每一个像素对应图像中的一个像素(否则会让图像在缩放到屏幕大小时变的模糊) LoadScene:加载不同关卡时,当前关卡的所有内容(场景中所有对象和所有附件的脚本)都会从内存中清除,接着从新场景中加载所有需要的对象 修改Transform.position将会忽略碰撞检测(Unity的碰撞用的第三方整合的,所以理论来说Transform系统和物理系统是两个单独的系统,所以移动操作应只利用其中一种) Mathf.Approximately(float, float):用于浮点数比较 Order in Layer设置:控制绘制前后顺序(同层Z轴控制前后) 平视显示(HUD,heads-up display):使图形叠加在世界视图上 OnGUI:立即模式的GUI系统,每帧显式发送绘制指令。会在每帧渲染完3D场景后执行 保留模式(retained mode):一次定义所有的视觉效果,系统知道会绘制什么,不需要重新声明 Canvas组件 创建Canvas组件时随带的EventSystem是用于UI交互的 Canvas中屏幕的一个像素对应的就是场景中的一个单位 Render Moad Screen Space-Overlay:将UI渲染为摄像机视图顶部的2D图形 Screen Space-Camera:将UI渲染在摄像机视图顶部,但UI元素可以旋转,得到透视效果 World Space:将画布对象放在场景中,就好像UI是3D场景的一部分 UI对象的锚点(anchor):对象依附到画布或屏幕的点,决定计算对象位置所依赖的点 切割图像(sliced image,九宫格):把图像切割成九份,各部分相互独立,分别缩放。从中间缩放图像边缘,可确保图像缩放为任何期望的尺寸,且边缘清晰 模型导入Model中Normals选项控制光线和阴影在模型上的显示 Import:使用定义在导入网格几何体中的法线 Calculate:让Unity自己计算每个多边形的法线 光照贴图(lightmapping):将几何体的阴影烘焙到贴图图像中 Mesh Renderer中可控制是否投射阴影和是否接受阴影 位置向量乘于四元数,结果是基于旋转的偏移位置 Vector3.ClampMagnitude:限制对角线,使其延轴移动的速度一样(从原点画圆) Animator中的动画过度 Has Exit Time:迫使动画一直播放,而不是在变换发生时马上暂停 Interruption Source改为Current State说明变换自身能被打断 美术资源 网格对象:指的是3D对象(连线和形状)的几何结构 模型:通常包括对象的其他属性 材质:一组信息,定义了附加材质对象的表面属性(颜色,发光等—)。多个对象可共享一个材质。每个材质都有一个控制它的着色器(可认为每种材质都是着色器的一个实例) 动画:定义关联对象的运动信息。运动能独立于对象自身定义,所以可以混合-匹配的方式用于多个对象(必须为同一种骨骼) 粒子系统:用于生成和控制大量运动对象的规则机制。运动对象通常较小所以被称为粒子系统。但并不一定要小 粒子:粒子系统控制的独立对象。可以是任何网络对象,但对于大多数效果,粒子显示图片的方块 贴图:用于提高3D图形效果的2D图像 用于显示在3D模型的表面 法线贴图使表面产生凹凸 贴图文件格式类型 优缺点 PNG 通常用于万维网。无损压缩,带透明 JPG 通常用于万维网。有损压缩,无透明 GIF 通常用于万维网。有损压缩,无透明(损耗并不是压缩造成的,是图片转为八位时导致数据丢失) BMP windows上默认模式。无压缩,无透明 TGA 通常用于3D图形(不常用)。无损压缩或不压缩,带透明 TIFF 通常用于数字相片和出版。无损压缩或不压缩,无透明 PICT 旧Macs系统默认格式。有损压缩,无透明 PSD Photoshop原生文件。无压缩,有透明 图像的压缩方式和是否有透明通道这两个是比较重要的因素 有透明通道会更好 不压缩和无损压缩保证质量,有损压缩降低了品质,但减小了文件大小(具体取舍有 贴图的大小应为2的次幂(从4~2048字节) 天空盒:是一个包围摄像机的立方体。每个面都是天空图片的贴图 着色器:一种简短的程序,列出了绘制表面的指令 一些Unity自带的着色器: Additive着色器:将粒子颜色叠加到它背后的颜色上的着色器,使颜色更加明亮,而粒子的黑色部分不可见。 Multiply着色器:与Additive相反,使对象颜色更暗 3D模型:美术资源可直接拖入引擎,unity会将文件导出为FBX然后重新加载到Unity中。尽量避免直接拖入美术资源,应将模型先导出为FBX后再直接导入到Unity为好。避免后续的资源修复问题和共享资源复杂等问题 可导入模型自带材质 3D模型文件类型 优缺点 FBX 网格和动画 Collada(DAE) 网格和动画 OBJ 只有网格;文本格式 3DS 只有网格 DXF 只有网格 使用粒子系统创建效果 粒子系统时创建和控制大量移动对象的规则机制</description>
</item>
<item>
<title>《游戏引擎架构》动画的基本构成</title>
<link>https://www.sooda.net.cn/posts/%E6%B8%B8%E6%88%8F%E5%BC%95%E6%93%8E%E6%9E%B6%E6%9E%84%E5%8A%A8%E7%94%BB%E7%9A%84%E5%9F%BA%E6%9C%AC%E6%9E%84%E6%88%90/</link>
<pubDate>Sun, 07 Nov 2021 17:14:24 +0800</pubDate>
<guid>https://www.sooda.net.cn/posts/%E6%B8%B8%E6%88%8F%E5%BC%95%E6%93%8E%E6%9E%B6%E6%9E%84%E5%8A%A8%E7%94%BB%E7%9A%84%E5%9F%BA%E6%9C%AC%E6%9E%84%E6%88%90/</guid>
<description>闲话 近日读完了《游戏引擎架构》这本书,以前感觉读不完,但是每天慢慢的磨完了。闲下来时间提炼一下其中自我觉得比较关键的部分。因本人是做Unity开发的,所以会把书中所讲部分结合Unity引擎来总结。闲话不多说开始吧
动画的基本构成 Unity的动画系统封装的已经非常好了,所以我们对其中的细节了解的少之又少。本文章着重在单个动画,骨骼,姿势,蒙皮的内存存储结构和应用方式
角色动画的类型 在Unity中比较常用的动画为蒙皮/骨骼动画(三维)和精灵动画(二维)
赛璐璐动画(精灵动画):用一张细小的位图,叠在全屏的背景影响之上而不会扰乱背景。常用于二维游戏动画(Unity中的Sprite贴图) 动画纹理:面向摄像机的四边形,并用一连串的位图连续播放。现今用于远景活低分辨率的物体 刚性阶层动画:将角色通过部位进行拆分建模并以层级进行约束。但问题是在关节处会出现裂缝 每顶点动画:移动每个顶点以产生更自然的动作(蛮力技术,数据量非常大。通常用于老式的离线渲染) 变形目标动画:每顶点动画的变种,也是制作每个顶点的位置。但制作少量的固定极端姿势在运行时将其混合(线性插值混合)。通常用于面部动画 蒙皮/骨骼动画: 骨骼:隐藏的刚性关节层阶结构(树结构)所构成 皮肤:绑定于骨骼上的圆滑三角形网格,顶点会按权重绑定至多个关节。当关节移动时,蒙皮可以自然的拉伸 为什么选择蒙皮/骨骼动画? 以更少的数据量来达到更好的效果。通过加入约束:相对大量的顶点只能跟随相对少量的骨骼关节移动。来压缩顶点动画
骨骼在内存中的表示 通常使用关节索引引用关节,子关节索引引用父关节。蒙皮三角形网格中,每个顶点索引引用其绑定关节
1// 关节数据的信息 2struct Joint { 3 Matrix4x3 inv_bind_pose; // 绑定姿势(蒙皮网格顶点绑定至骨骼时,关节的位置,定向及缩放)的逆变换 4 const char* name; // 关节名字(字符串或32位字符串散列表标识符) 5 U8 parent; // 父索引(0xFF代表根关节) 6}; 7 8struct Skeletion { 9 U32 joint_count; // 关节数目 10 Joint* joint; // 关节数组 11}; 姿势 把角色摆出一连串离散,静止的姿势,并以通常30或60个姿势每秒的速率显示,已产生动感。实际游戏会以相邻姿势进行插值
绑定姿势:又称为T姿势。因此姿势四肢远离身体,较容易把顶点绑定至关节
局部姿势 相对于父关节指定的,其仿射变换相对于父节点空间
关节姿势:数学上就是一个仿射变换。4x4仿射变换矩阵$P_j$,此矩阵由平移矢量$T_j$,3x3对角缩放矩阵$S_j$,及3x3旋转矩阵$R_j$构成
$$ p_j = \begin{bmatrix} S_jR_j &amp; 0 \newline T_j &amp; 1 \end{bmatrix} $$</description>
</item>
</channel>
</rss>