-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathatom.xml
More file actions
1262 lines (1194 loc) · 61.3 KB
/
atom.xml
File metadata and controls
1262 lines (1194 loc) · 61.3 KB
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
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
<id>https://heanrum.github.io</id>
<title>秋收冬藏</title>
<updated>2025-04-26T03:04:30.023Z</updated>
<generator>https://github.com/jpmonette/feed</generator>
<link rel="alternate" href="https://heanrum.github.io"/>
<link rel="self" href="https://heanrum.github.io/atom.xml"/>
<subtitle>温故而知新</subtitle>
<logo>https://heanrum.github.io/images/avatar.png</logo>
<icon>https://heanrum.github.io/favicon.ico</icon>
<rights>All rights reserved 2025, 秋收冬藏</rights>
<entry>
<title type="html"><![CDATA[Roslyn代码生成和语法分析在Unity中的应用]]></title>
<id>https://heanrum.github.io/post/roslyn-dai-ma-sheng-cheng-he-yu-fa-fen-xi-zai-unity-zhong-de-ying-yong/</id>
<link href="https://heanrum.github.io/post/roslyn-dai-ma-sheng-cheng-he-yu-fa-fen-xi-zai-unity-zhong-de-ying-yong/">
</link>
<updated>2025-04-26T02:18:24.000Z</updated>
<content type="html"><![CDATA[<!-- more -->
<h3 id="1rosyln介绍">1.Rosyln介绍</h3>
<p>Roslyn是微软推出的开源.NET编译器平台,仓库地址:https://github.com/dotnet/Roslyn,其核心特点在于将传统“黑盒”式的编译器转变为开放的API服务。使得开发者能够参与编译过程,甚至修改编译结果,为开发者提供深度的代码分析、动态编译及代码生成能力。</p>
<h3 id="2在unity中的应用">2.在unity中的应用</h3>
<p>在unity中的样例可以参考unity的官方文档:https://docs.unity3d.com/Manual/roslyn-analyzers.html。需要注意的是想要在unity editor中使用代码生成和代码分析的话,在2020.2版本之后才生效。</p>
<h4 id="21-如何只在ide中使用代码分析">2.1 如何只在IDE中使用代码分析</h4>
<p>值得一提的是虽然unity editor中的代码编译的报错警告等信息通常和ide中的一样,但是这这两者实际上是独立进行的代码分析,例如vscode是依赖csproj来进行分析的,这也是有时候需要手动在unity editor的preference中手动点击重新生成csproj的原因,所以理论上可以让两者的代码分析结果不一样。</p>
<p>通过unity的官方case我们知道,把生成的Analyzer的dll手动添加RoslynAnalyzer的tag,就可以让代码分析同时在unity editor和ide中生效。在ide中生效的原因是unity每次重新生成对应csproj的时候都会把有RoslynAnalyzer tag的对应项生成到csproj中,例如我们添加了一个名为UnityRoslyn的dll,csproj中就会有如下的项:</p>
<pre><code class="language-json"> <ItemGroup>
<Analyzer Include="xxxAssets\Plugins\UnityRoslyn.dll" />
</ItemGroup>
</code></pre>
<p>这样ide在进行代码分析的时候就会使用这个dll来进行代码分析。</p>
<p>那么对于比较老版本的unity不支持RoslynAnalyzer的tag,或者只想在ide中进行代码分析,避免editor编译时间变长应该如何配置呢。</p>
<p>首先csproj是由unity生成的,我们手动向里面添加对应的itemgroup是不行的,因为下次生成又会被自动生成的覆盖。解决方案是借助unity的asset后处理接口,在生成csproj的时候手动把这些itemgroup给添加上去:</p>
<pre><code class="language-csharp">public class ProjectFilePostprocessor : AssetPostprocessor
{
public static string OnGeneratedSlnSolution(string path, string content)
{
Debug.Log("OnGeneratedSlnSolution: " + path);
return content;
}
public static string OnGeneratedCSProject(string path, string content)
{
Debug.Log("OnGeneratedCSProject: "+path);
int index = content.LastIndexOf("</ItemGroup>");
if (index != -1)
{
StringBuilder sb = new StringBuilder();
sb.Append(string.Format(" <Analyzer Include=\"{0}/GameMain/Plugins/ErrorProne.NET.Core.dll\" />\n", Application.dataPath));
sb.Append(string.Format(" <Analyzer Include=\"{0}/GameMain/Plugins/ErrorProne.Net.CoreAnalyzers.dll\" />\n", Application.dataPath));
sb.Append(string.Format(" <Analyzer Include=\"{0}/GameMain/Plugins/RuntimeContracts.dll\" />\n", Application.dataPath));
content = content.Insert(index, sb.ToString());
}
return content;
}
}
</code></pre>
<p>在unity重新生成sln和csproj的时候会调用到OnGeneratedSlnSolution和OnGeneratedCSProject方法,因此我们在对应的方法中添加对应的Analyzer项即可。</p>
<h3 id="3踩坑记录">3.踩坑记录</h3>
<p>我们可以通过ruleset来指定rule的严重等级,但是在vscode中手动修改rule的Action之后不会立即生效,需要reload一下或者重启vscode才会生效。</p>
]]></content>
</entry>
<entry>
<title type="html"><![CDATA[emmylua调试器原理]]></title>
<id>https://heanrum.github.io/post/emmylua-diao-shi-qi-yuan-li/</id>
<link href="https://heanrum.github.io/post/emmylua-diao-shi-qi-yuan-li/">
</link>
<updated>2025-03-29T03:16:09.000Z</updated>
<content type="html"><![CDATA[<h1 id="1lua语言的调试支持">1.lua语言的调试支持</h1>
<p>分析emmylua调试器原理之前首先介绍一下lua自身对调试的支持,便于后续理解。下面的lua源码和接口均基于lua5.3,和其它版本的lua可能会略有偏差。</p>
<h2 id="11-设置hook">1.1 设置Hook</h2>
<p>我们可以通过**<code>void lua_sethook (lua_State *L, lua_Hook f, int mask, int count)</code>**给lua的指定线程设置回调函数,当满足指定条件时,设置的回调函数就会被自动调用,参数中的<code>mask</code>即条件包含下面几种:</p>
<ul>
<li>LUA_MASKCALL:某个函数调用时触发。</li>
<li>LUA_MASKRET:某个函数返回时触发。</li>
<li>LUA_MASKLINE:执行新的一行代码时触发。</li>
<li>LUA_MASKCOUNT:执行指定数量指令后触发。</li>
</ul>
<p>lua_sethook的实现如下,可以看到其实就是在对lua_State的字段进行一些操作,把我们传入的hook函数和触发时机都设置到了lua_State的对应字段上。</p>
<pre><code class="language-c">LUA_API void lua_sethook (lua_State *L, lua_Hook func, int mask, int count) {
if (func == NULL || mask == 0) { /* turn off hooks? */
mask = 0;
func = NULL;
}
if (isLua(L->ci))
L->oldpc = L->ci->u.l.savedpc;
L->hook = func;
L->basehookcount = count;
resethookcount(L);
L->hookmask = cast_byte(mask);
}
</code></pre>
<h2 id="12-hook触发">1.2 Hook触发</h2>
<p>通过lua_sethook我们可以指定在某些条件下,例如每执行一行新代码时(LUA_MASKLINE)调用我们设置的回调,但是这个回调是如何被触发调用的呢。在lua5.3的实现中,有个**<code>luaV_execute</code>**函数,作用是逐条解析和执行lua编译后的字节码。</p>
<pre><code class="language-c">void luaV_execute (lua_State *L) {
...
/* main loop of interpreter */
for (;;) {
Instruction i;
StkId ra;
vmfetch(); // 尝试调用hook
vmdispatch (GET_OPCODE(i)) {
...
}
...
}
</code></pre>
<p>vmdispatch的作用就是根据当前的字节码的opcode跳转到对应操作的具体执行逻辑,而在执行具体的字节码之前会调用一个**<code>vmfetch</code><strong>函数,vmfetch中会判断如果hookmask有LUA_MASKLINE或LUA_MASKCOUNT标记则会调用</strong><code>luaG_traceexec</code>**判断是否需要触发hook函数。</p>
<pre><code class="language-c">#define vmfetch() { \
i = *(ci->u.l.savedpc++); \
if (L->hookmask & (LUA_MASKLINE | LUA_MASKCOUNT)) \
Protect(luaG_traceexec(L)); \
ra = RA(i); /* WARNING: any stack reallocation invalidates 'ra' */ \
lua_assert(base == ci->u.l.base); \
lua_assert(base <= L->top && L->top < L->stack + L->stacksize); \
}
</code></pre>
<p>**<code>luaG_traceexec</code>**的主要作用就是根据设置hook触发条件来判断当前是否可以触发hook调用,例如如果设置了LUA_MASKCOUNT会判断当前调用次数是否达到目标,如果满足条件则会触发hook函数调用。</p>
<pre><code class="language-c">void luaG_traceexec (lua_State *L) {
CallInfo *ci = L->ci;
lu_byte mask = L->hookmask;
int counthook = (--L->hookcount == 0 && (mask & LUA_MASKCOUNT));
if (counthook)
resethookcount(L); /* reset count */
else if (!(mask & LUA_MASKLINE))
return; /* no line hook and count != 0; nothing to be done */
if (ci->callstatus & CIST_HOOKYIELD) { /* called hook last time? */
ci->callstatus &= ~CIST_HOOKYIELD; /* erase mark */
return; /* do not call hook again (VM yielded, so it did not move) */
}
if (counthook)
luaD_hook(L, LUA_HOOKCOUNT, -1); /* call count hook */
if (mask & LUA_MASKLINE) {
Proto *p = ci_func(ci)->p;
int npc = pcRel(ci->u.l.savedpc, p);
int newline = getfuncline(p, npc);
if (npc == 0 || /* call linehook when enter a new function, */
ci->u.l.savedpc <= L->oldpc || /* when jump back (loop), or when */
newline != getfuncline(p, pcRel(L->oldpc, p))) /* enter a new line */
luaD_hook(L, LUA_HOOKLINE, newline); /* call line hook */
}
L->oldpc = ci->u.l.savedpc;
if (L->status == LUA_YIELD) { /* did hook yield? */
if (counthook)
L->hookcount = 1; /* undo decrement to zero */
ci->u.l.savedpc--; /* undo increment (resume will increment it again) */
ci->callstatus |= CIST_HOOKYIELD; /* mark that it yielded */
ci->func = L->top - 1; /* protect stack below results */
luaD_throw(L, LUA_YIELD);
}
}
</code></pre>
<h2 id="13-获取调试信息">1.3 获取调试信息</h2>
<p>现在我们已经知道了如何在lua运行中插入hook函数,从而在运行一行新的代码的时候执行我们自己的函数,那么我们如何在hook函数中获取到我们想要的信息呢?接下来就介绍lua提供的调试相关的数据接口和接口。</p>
<h3 id="131-lua_debug结构体">1.3.1 lua_Debug结构体</h3>
<pre><code class="language-c">typedef struct lua_Debug {
int event;
const char *name; /* (n) */
const char *namewhat; /* (n) */
const char *what; /* (S) */
const char *source; /* (S) */
int currentline; /* (l) */
int linedefined; /* (S) */
int lastlinedefined; /* (S) */
unsigned char nups; /* (u) number of upvalues */
unsigned char nparams; /* (u) number of parameters */
char isvararg; /* (u) */
char istailcall; /* (t) */
char short_src[LUA_IDSIZE]; /* (S) */
/* private part */
other fields
} lua_Debug;
</code></pre>
<p>在lua的源码实现中有一个用于辅助调试的结构体lua_Debug,其中存储一个函数或者一个记录A(activation record)的信息。</p>
<p>关键字段的具体含义如下:</p>
<ul>
<li>source:创建这个函数的chunk名称</li>
<li>currentline:给定函数当前正在运行的代码行号</li>
<li>name:给定函数的合理(reasonable)的名字。因为函数在lua中是第一类值,可以是个匿名函数,可以被保存到变量或者表中,不一定会有一个定义的名字。</li>
<li>nups:函数的上值数量</li>
</ul>
<p>每个字段后面的(n)类似的注释可以理解为是对这个字段的分类,在通过<code>int lua_getinfo (lua_State *L, const char *what, lua_Debug *ar)</code>获取lua_Debug的时候可以通过what字段指定分类,从而获取到填充了对应分类字段数据的lua_Debug。</p>
<h3 id="132-lua_hook定义">1.3.2 lua_Hook定义</h3>
<p>我们可以通过**<code>void lua_sethook (lua_State *L, lua_Hook f, int mask, int count)</code><strong>给指定线程设置一个lua_Hook类型的回调,lua_Hook类型的定义如下:</strong><code>typedef void (*lua_Hook) (lua_State *L, lua_Debug *ar)</code>**,可以看到lua_Hook还接收一个lua_Debug的指针ar,这个ar中就保存了当前函数调用的信息以便我们在lua_Hook中获取和使用。</p>
<p>值得一提的是,在我们实现的lua_Hook中可以调用lua的C API来执行一些lua代码,意味着lua虚拟机仍然会正常运行,即<code>luaV_execute</code>还是会调用,但是lua在运行一个hook函数的时候,禁用了其它hook的调用,此时无法调用新的hook函数。</p>
<p>另外,lua_Hook函数不支持延续(continuations),不能用lua_yieldk等方式挂起,只能通过lua_yield类似方式挂起。</p>
<h1 id="2emmylua调试器原理">2.emmylua调试器原理</h1>
<p>emmylua调试器的仓库地址:https://github.com/EmmyLua/EmmyLuaDebugger,下面开始分析emmylua是如何实现断点,单步步进,监视变量等功能的。</p>
<h2 id="21-调试框架">2.1 调试框架</h2>
<p>emmylua的调试框架可以认为是一个cs模式,ide侧的插件是client,负责收集用户的输入(断点信息,监视的变量,step in/step over等)然后发送给debugger请求对应的数据。而lua虚拟机侧是server,负责接收来自ide的请求,然后从lua虚拟机中获取相关数据,或者暂停lua虚拟机的执行等,然后把数据返回给ide展示。上述仓库中的代码就是lua虚拟机侧执行的代码,主要负责判断断点是否命中,获取监视变量的结果,修改指定变量的值,处理用于的step in/step over等命令。emmylua插件侧的代码在Emmylua的其他仓库中可以找到,这里不再列举。</p>
<p>使用这种结构有个好处就是多个不同的ide可以共用一套debugger代码,例如emmylua的vscode插件和rider插件是两套逻辑,因为这两个ide的前端表现的接口有很大的区别,但是它们使用的后端debugger逻辑是相同的,节省了开发成本。下面提到的ide操作均为vscode下,与rider的可能略有区别,但是debugger的逻辑是相同的,不影响对debugger的理解。</p>
<h2 id="22-调试器连接">2.2 调试器连接</h2>
<p>emmylua的插件端和debugger之间可以使用tcp来进行通信,首先需要我们在lua虚拟机侧加载和开启tcpListen。</p>
<pre><code class="language-c"> EMMY_CORE_EXPORT int luaopen_emmy_core(struct lua_State* L) {
EmmyFacade::Get().SetWorkMode(WorkMode::EmmyCore);
if (!install_emmy_debugger(L))
return false;
luaL_newlibtable(L, lib);
luaL_setfuncs(L, lib, 0);
// _G.emmy_core
lua_pushglobaltable(L);
lua_pushstring(L, "emmy_core");
lua_pushvalue(L, -3);
lua_rawset(L, -3);
lua_pop(L, 1);
return 1;
}
</code></pre>
<p>我们在项目启动lua虚拟机之后,通过<code>require("emmy_core")</code>加载编译好的emmy_core.dll,加载dll的同时lua会自动调用luaopen_emmy_core函数,这个函数的作用主要是把emmy_core设置为全局变量,同时给这个全局变量增加了下面几个函数:</p>
<pre><code class="language-c">static const luaL_Reg lib[] = {
{"tcpListen", tcpListen},
{"tcpConnect", tcpConnect},
{"pipeListen", pipeListen},
{"pipeConnect", pipeConnect},
{"waitIDE", waitIDE},
{"breakHere", breakHere},
{"stop", stop},
{"tcpSharedListen", tcpSharedListen},
{"registerTypeName", registerTypeName},
{nullptr, nullptr}
};
</code></pre>
<p>require emmy_core之后在lua中执行emmy_core.tcpListen(),传入对应的ip和端口号就开启一个tcp监听了。</p>
<p>之后我们需要设置vscode里的调试配置,注意这里的端口号需要和我们vscode里调试器的配置端口号保持一致,vscode中默认的端口号使用的是9966。设置好了之后点击开始调试即可连接到emmylua debugger开始debug了。</p>
<pre><code class="language-json"> {
"type": "emmylua_new",
"request": "launch",
"name": "EmmyLua New Debug",
"host": "localhost",
"port": 9966, // 需要和tcpListen中的端口号一致
"ext": [
".lua",
".lua.txt",
".lua.bytes"
],
"ideConnectDebugger": true
},
</code></pre>
<h2 id="23-设置hook">2.3 设置Hook</h2>
<p>在开启tcpListen的同时,emmylua会通过<code>SetReadyHook</code>来给当前线程设置Hook</p>
<pre><code class="language-c++">void EmmyFacade::SetReadyHook(lua_State *L) {
lua_sethook(L, ReadyLuaHook, LUA_MASKCALL | LUA_MASKLINE | LUA_MASKRET, 0);
}
</code></pre>
<p>这里设置的Hook函数<code>ReadyLuaHook</code>还不会开始处理断点相关的调试逻辑,而是进一步设置Hook。</p>
<pre><code class="language-c++">void EmmyFacade::ReadyLuaHook(lua_State *L, lua_Debug *ar) {
if (!Get().readyHook) {
return;
}
Get().readyHook = false;
auto states = FindAllCoroutine(L); // 获取虚拟机中所有协程
for (auto state: states) {
lua_sethook(state, HookLua, LUA_MASKCALL | LUA_MASKLINE | LUA_MASKRET, 0);
}
lua_sethook(L, HookLua, LUA_MASKCALL | LUA_MASKLINE | LUA_MASKRET, 0);
auto debugger = Get().GetDebugger(L);
if (debugger) {
debugger->Attach();
}
Get().Hook(L, ar);
}
</code></pre>
<p>这段代码主要逻辑就是找到当前lua虚拟机中所有的lua_State,然后把给它们都设置上<code>Hooklua</code>的Hook回调。这么做是为了避免因为有代码运行在没有设置hook函数的协程中,导致无法调试相关代码。</p>
<p>注意这里只需要把当前的所有lua_State都设置上hook,那么之后即使有新创建的协程,对应的hook也会被自动设置上,这是因为lua在创建新的lua_State的时候会把创建者的hook信息复制给新创建的lua_State,对应代码在<code>lua_newthread</code>中</p>
<pre><code class="language-c">LUA_API lua_State *lua_newthread (lua_State *L) {
...
// 把当前lua_State中的hook信息复制给新创建的lua_State
L1->hookmask = L->hookmask;
L1->basehookcount = L->basehookcount;
L1->hook = L->hook;
resethookcount(L1);
...
}
</code></pre>
<h2 id="24-断点命中">2.4 断点命中</h2>
<h3 id="241-断点匹配">2.4.1 断点匹配</h3>
<p>在设置了hook之后,lua虚拟机每次执行代码都会回调我们设置的hook函数<code>HookLua</code>。<code>HookLua</code>主要逻辑就是找到当前lua_State对应的Debugger,然后调用<code>Debugger:Hook</code>,断点匹配相关逻辑就在这个函数中。</p>
<pre><code class="language-cpp">void Debugger::Hook(lua_Debug *ar, lua_State *L) {
...
auto bp = FindBreakPoint(ar);
if (bp && ProcessBreakPoint(bp)) {
HandleBreak();
return;
}
...
}
</code></pre>
<p><code>FindBreakPoint</code>的代码如下,首先获取到当前执行的代码行号,然后检查缓存的所有断点的代码行号集合<code>lineSet</code>中是否有当前行号,如果没有则断点未命中直接返回。这一步是一个粗略的剪枝,可以过滤大部分情况。</p>
<p>如果<code>lineSet</code>中有当前行号,则进一步判断当前执行的文件名是否和断点中的文件名匹配。首先通过<code>lua_getinfo</code>获取到当前执行代码的文件名,接着通过<code>FindBreakPoint</code>的重载函数检查是否有断点的文件名和行号和当前执行的代码文件名和行号匹配。</p>
<pre><code class="language-cpp">std::shared_ptr<BreakPoint> Debugger::FindBreakPoint(lua_Debug *ar) {
if (!currentL) {
return nullptr;
}
auto L = currentL;
const int cl = getDebugCurrentLine(ar);
auto lineSet = manager->GetLineSet();
if (cl >= 0 && lineSet.find(cl) != lineSet.end()) {
lua_getinfo(L, "S", ar);
const auto chunkname = GetFile(ar);
return FindBreakPoint(chunkname, cl);
}
return nullptr;
}
</code></pre>
<p><code>FindBreakPoint</code>的重载函数主要逻辑就是遍历所有的断点,逐个检查文件名和行号是否和当前执行的代码行号和文件名匹配,如果匹配则返回对应的断点。<code>FuzzyMatchFileName</code>主要逻辑就是检查文件是否匹配,从<code>lua_getinfo</code>中获取到的文件名可能是require/dofile等方式传入的,会有aaa/./bbb等非绝对路径的情况,而断点中保存的文件名是由前端插件记录的,是一个绝对路径,因此需要处理一下文件名匹配和后缀匹配,具体逻辑这里不展示了。</p>
<pre><code class="language-cpp">std::shared_ptr<BreakPoint> Debugger::FindBreakPoint(const std::string &chunkname, int line) {
std::shared_ptr<BreakPoint> breakedpoint = nullptr;
int maxMatchProcess = 0;
auto breakpoints = manager->GetBreakpoints();
for (const auto bp: breakpoints) {
if (bp->line == line) {
// fuzz match: bp(x/a/b/c), file(a/b/c)
int matchProcess = FuzzyMatchFileName(chunkname, bp->file);
if (matchProcess > 0 && matchProcess > maxMatchProcess) {
maxMatchProcess = matchProcess;
breakedpoint = bp;
}
}
}
return breakedpoint;
}
</code></pre>
<h3 id="242-断点条件判断">2.4.2 断点条件判断</h3>
<p><code>Debugger::Hook</code>的源码中可以看到,当断点的所在文件和行号均和当前执行的情况匹配会返回一个bp的数据结构,还需要<code>ProcessBreakPoint</code>的返回值为true才能进入break的逻辑。<code>ProcessBreakPoint</code>的作用就是检查断点的条件是否匹配,以支持条件断点,次数断点,命中时输出log等功能,具体逻辑如下:</p>
<pre><code class="language-cpp">bool Debugger::ProcessBreakPoint(std::shared_ptr<BreakPoint> bp) {
if (!bp->condition.empty()) {
auto ctx = std::make_shared<EvalContext>();
ctx->expr = bp->condition;
ctx->depth = 1;
bool suc = DoEval(ctx);
return suc && ctx->result->valueType == LUA_TBOOLEAN && ctx->result->value == "true";
}
if (!bp->logMessage.empty()) {
DoLogMessage(bp);
return false;
}
if (!bp->hitCondition.empty()) {
bp->hitCount++;
return DoHitCondition(bp);
}
return true;
}
</code></pre>
<p>可以看到<code>ProcessBreakPoint</code>的逻辑主要有3个部分,如果断点配置了条件,则验证对应的条件是否满足,具体验证逻辑就是通过<code>DoEval</code>计算给定条件表达式的值是否为true,<code>DoEval</code>的实现后续会介绍,目前可以认为就是对一个表达式求值。如果配置了logMessage则输出对应信息到控制台,并返回断点未命中。如果配置了命中次数则检查命中次数是否达标。</p>
<p>当断点通过<code>ProcessBreakPoint</code>的检查后才会被认为是真正命中,接下来就需要在ide中展示命中的断点和相关的堆栈信息,这些逻辑是在<code>HandleBreak</code>的断点处理函数中进行处理的。</p>
<h2 id="25-断点信息">2.5 断点信息</h2>
<p>断点命中之后会调用到<code>OnBreak()</code>中,这个函数一个主要作用是通过<code>GetStacks</code>获取当前的调用栈和局部变量等信息,用于显示在ide中,另一个作用就是通过tcp连接通知ide当前命中的断点。</p>
<pre><code class="language-cpp">bool EmmyFacade::OnBreak(std::shared_ptr<Debugger> debugger) {
if (!debugger) {
return false;
}
std::vector<Stack> stacks;
_emmyDebuggerManager.SetHitDebugger(debugger);
// 获取局部变量和栈信息
debugger->GetStacks(stacks);
auto obj = nlohmann::json::object();
obj["cmd"] = static_cast<int>(MessageCMD::BreakNotify);
obj["stacks"] = JsonProtocol::SerializeArray(stacks);
// 通知ide断点命中
transporter->Send(int(MessageCMD::BreakNotify), obj);
return true;
}
</code></pre>
<p><code>GetStacks</code>的函数声明为<code>bool Debugger::GetStacks(std::vector<Stack> &stacks)</code>,调用之后可以得到当前运行的函数的堆栈信息和对应的所有局部变量,用于显示在ide中,具体代码如下</p>
<pre><code class="language-cpp">bool Debugger::GetStacks(std::vector<Stack> &stacks) {
if (!currentL) {
return false;
}
auto prevCurrentL = currentL;
auto L = currentL;
int totalLevel = 0;
while (true) {
int level = 0;
while (true) {
lua_Debug ar{};
// 获取栈信息
if (!lua_getstack(L, level, &ar)) {
break;
}
// 获取栈上的其它信息
if (!lua_getinfo(L, "nSlu", &ar)) {
continue;
}
// C++ 17 only return T&
stacks.emplace_back();
auto &stack = stacks.back();
stack.file = GetFile(&ar);
stack.functionName = getDebugName(&ar) == nullptr ? "" : getDebugName(&ar);
stack.level = totalLevel++;
stack.line = getDebugCurrentLine(&ar);
// get variables
{
// 获取局部变量
for (int i = 1;; i++) {
const char *name = lua_getlocal(L, &ar, i);
if (name == nullptr) {
break;
}
if (name[0] == '(') {
lua_pop(L, 1);
continue;
}
// add local variable
auto var = stack.variableArena->Alloc();
var->name = name;
SetVariableArena(stack.variableArena.get());
GetVariable(L, var, -1, 1);
ClearVariableArenaRef();
lua_pop(L, 1);
stack.localVariables.push_back(var);
}
// 获取上值信息
if (lua_getinfo(L, "f", &ar)) {
const int fIdx = lua_gettop(L);
for (int i = 1;; i++) {
const char *name = lua_getupvalue(L, fIdx, i);
if (!name) {
break;
}
// add up variable
auto var = stack.variableArena->Alloc();
var->name = name;
SetVariableArena(stack.variableArena.get());
GetVariable(L, var, -1, 1);
ClearVariableArenaRef();
lua_pop(L, 1);
stack.upvalueVariables.push_back(var);
}
// pop function
lua_pop(L, 1);
}
}
level++;
}
// TODO
lua_State *PL = manager->extension.QueryParentThread(L);
if (PL != nullptr) {
L = PL;
} else {
break;
}
}
SetCurrentState(prevCurrentL);
return false;
}
</code></pre>
<p>主要逻辑是由两个while(true)循环包裹起来的,里层的while循环就是在从底向上获取栈的信息,外层的while循环是在从子线程到父线程遍历。</p>
<p>具体来看里层的循环逻辑,首先是通过<code>lua_getstack</code>获取当前level的栈信息,当level为0时即获取当前在调用的函数栈,level为1则是调用level 0的函数的栈,以此类推,直到遍历到栈顶。获取到栈信息后继续通过<code>lua_getinfo</code>来获取当前的行号,文件名等其它信息。</p>
<p>下面的一个<code>for (int i = 1;; i++) </code>循环是在逐个获取当前栈中的局部变量,通过<code>const char *lua_getlocal (lua_State *L, const lua_Debug *ar, int n)</code>可以把第n个局部变量push到栈上,并返回它的名字字符串。接着通过自己封装的<code>GetVariable</code>来获取这个变量的可表示形式,例如如果变量是一个字符串则获取它的值,如果是一个table则获取它所有的键值对,关于<code>GetVariable</code>的具体实现后续再展开。</p>
<p>接着通过一个<code>for (int i = 1;; i++) </code>循环来遍历获取当前函数的所有上值,获取到之后也是通过<code>GetVariable</code>来获取每个上值的表示。</p>
<h2 id="26-断点操作">2.6 断点操作</h2>
<p>当命中一个断点后,程序会被暂停,调试器等待用户输入进行下一步的行为。常用的操作有Step Over,Step In,Step Out,Stop,添加监视表达式等,下面逐一来看这些操作是如何实现的。</p>
<h3 id="261-暂停程序">2.6.1 暂停程序</h3>
<p>当命中断点之后,emmylua会调用到<code>EnterDebugMode()</code>,表示自己进入调试模式,代码如下:</p>
<pre><code class="language-cpp">void Debugger::EnterDebugMode() {
std::unique_lock<std::mutex> lock(runMtx);
blocking = true;
while (true) {
std::unique_lock<std::mutex> lockEval(evalMtx);
if (evalQueue.empty() && blocking) {
lockEval.unlock();
cvRun.wait(lock);
lockEval.lock();
}
if (!evalQueue.empty()) {
const auto evalContext = evalQueue.front();
evalQueue.pop();
lockEval.unlock();
const bool skip = skipHook;
skipHook = true;
evalContext->success = DoEval(evalContext);
skipHook = skip;
EmmyFacade::Get().OnEvalResult(evalContext);
continue;
}
break;
}
ClearCache();
}
</code></pre>
<p>可以看到当blocking为true时,这是一个死循环。若evalQueue不为空,则会逐个求evalQueue中的值直到为空。若evalQueue为空则会调用<code>cvRun.wait(lock)</code>,cvRun的类型是<code>condition_variable</code>,因此这里就是在等待<code>cvRun</code>满足条件。查阅代码可以发现<code>cvRun</code>满足条件的情况只有两种,一种是推出调试模式,即用户手动选择stop等情况,另一种则是由于需要求取某个变量的值,向evalQueue中push了一个变量。</p>
<pre><code class="language-cpp">void Debugger::ExitDebugMode() {
blocking = false;
cvRun.notify_all();
}
bool Debugger::Eval(std::shared_ptr<EvalContext> evalContext, bool force) {
if (force)
return DoEval(evalContext);
if (!blocking) {
return false;
}
// 加锁
{
std::unique_lock<std::mutex> lock(evalMtx);
evalQueue.push(evalContext);
}
cvRun.notify_all();
return true;
}
</code></pre>
<p>从1.2中我们知道lua虚拟机会先调用hook函数,然后才执行对应的字节码,而emmylua的hook函数中是一个循环,所以lua虚拟机相当于被暂停了,无法执行后续指令。</p>
<h3 id="262-step-over">2.6.2 Step Over</h3>
<p>在ide中选择执行了相应的操作之后,ide就会发送指定到debugger,debugger这边主要通过<code>DoAction</code>来分发执行:</p>
<pre><code class="language-cpp">void Debugger::DoAction(DebugAction action) {
// 锁加到这里
std::lock_guard<std::mutex> lock(hookStateMtx);
switch (action) {
case DebugAction::Break:
SetHookState(manager->stateBreak);
break;
case DebugAction::Continue:
SetHookState(manager->stateContinue);
break;
case DebugAction::StepOver:
SetHookState(manager->stateStepOver);
break;
case DebugAction::StepIn:
SetHookState(manager->stateStepIn);
break;
case DebugAction::Stop:
SetHookState(manager->stateStop);
break;
case DebugAction::StepOut:
SetHookState(manager->stateStepOut);
break;
default:
break;
}
}
void Debugger::SetHookState(std::shared_ptr<HookState> newState) {
if (!currentL) {
return;
}
auto L = currentL;
hookState = nullptr;
if (newState->Start(shared_from_this(), L)) {
hookState = newState;
}
}
</code></pre>
<p>可以看到这里不管执行哪个命令,实际上都是在调用对应命令的State中实现的<code>Start</code>方法,然后记录当前的hookState为对应状态。</p>
<p>下面查看Step Over的具体实现:</p>
<pre><code class="language-cpp">bool HookStateStepOver::Start(std::shared_ptr<Debugger> debugger, lua_State* current)
{
if (!StackLevelBasedState::Start(debugger, current))
return false;
lua_Debug ar{};
lua_getstack(current, 0, &ar);
lua_getinfo(current, "nSl", &ar);
file = getDebugSource(&ar);
line = getDebugCurrentLine(&ar);
debugger->ExitDebugMode();
return true;
}
</code></pre>
<p>可以看到这里就是记录了当前的文件名和行号,然后退出调试模式,使得lua虚拟机能够继续向前执行。当lua虚拟机执行一条命令之后就又会进入emmylua的Hook函数:</p>
<pre><code class="language-cpp">void Debugger::Hook(lua_Debug *ar, lua_State *L) {
...
auto bp = FindBreakPoint(ar);
if (bp && ProcessBreakPoint(bp)) {
HandleBreak();
return;
}
// 加锁
std::shared_ptr<HookState> state = nullptr;
{
std::lock_guard<std::mutex> lock(hookStateMtx);
state = hookState;
}
if (state) {
state->ProcessHook(shared_from_this(), currentL, ar);
}
}
</code></pre>
<p>在加锁之后会调用到StepOverState的ProcessHook:</p>
<pre><code class="language-c++">void HookStateStepOver::ProcessHook(std::shared_ptr<Debugger> debugger, lua_State* L, lua_Debug* ar)
{
UpdateStackLevel(debugger, L, ar);
// step out
if (newStackLevel < oriStackLevel)
{
debugger->HandleBreak();
return;
}
if (getDebugEvent(ar) == LUA_HOOKLINE &&
getDebugCurrentLine(ar) != line &&
newStackLevel == oriStackLevel)
{
lua_getinfo(L, "Sl", ar);
if (getDebugSource(ar) == file || line == -1)
{
debugger->HandleBreak();
return;
}
}
StackLevelBasedState::ProcessHook(debugger, L, ar);
}
</code></pre>
<p>可以看到StepOverState处理hook的方式就是当跳出当前函数或者执行到当前函数新的一行时调用HandleBreak重新进入调试模式。</p>
<p>综上所述,Step Over的处理方式就是先退出调试模式,让lua虚拟机继续执行后续代码,随后又重新进入调试模式,从而实现了单步执行的功能。</p>
<h3 id="263-step-in">2.6.3 Step In</h3>
<p>step in的功能是让断点跳进当前函数的函数体,<code>HookStateStepIn::Start</code>逻辑和StepOver的完全一致,就是记录当前的文件名和行号之后退出调试模式,此处不再展示。</p>
<p>ProcessHook逻辑如下:</p>
<pre><code class="language-cpp">void HookStateStepIn::ProcessHook(std::shared_ptr<Debugger> debugger, lua_State* L, lua_Debug* ar)
{
UpdateStackLevel(debugger, L, ar);
if (getDebugEvent(ar) == LUA_HOOKLINE)
{
lua_getinfo(L, "nSl", ar);
auto currentLine = getDebugCurrentLine(ar);
auto source = getDebugSource(ar);
if(currentLine != line || file != source)
{
debugger->HandleBreak();
}
return;
}
StackLevelBasedState::ProcessHook(debugger, L, ar);
}
</code></pre>
<p>可以看到step in和step over不同点就是在于触发重进断点的条件不同,step in只要行号或者执行的函数名不同就会进入断点,而step over条件会更严格,必须要执行到当前函数的不同行或者执行到上层函数才会重进断点。</p>
<h3 id="264-step-out">2.6.4 Step Out</h3>
<pre><code class="language-cpp">bool HookStateStepOut::Start(std::shared_ptr<Debugger> debugger, lua_State* current)
{
if (!StackLevelBasedState::Start(debugger, current))
return false;
debugger->ExitDebugMode();
return true;
}
</code></pre>
<p>step out的Start逻辑更加简单,因为不需要行号和当前函数信息,只需要通过<code>StackLevelBasedState::Start</code>记录下当前的栈level信息,之后退出调试模式即可。</p>
<pre><code class="language-cpp">void HookStateStepOut::ProcessHook(std::shared_ptr<Debugger> debugger, lua_State* L, lua_Debug* ar)
{
UpdateStackLevel(debugger, L, ar);
if (newStackLevel < oriStackLevel)
{
debugger->HandleBreak();
return;
}
StackLevelBasedState::ProcessHook(debugger, L, ar);
}
</code></pre>
<p>重进断点的逻辑也相当简略,只需要当前执行到的栈level比记录时的level低即可,即跳到上层函数。</p>
]]></content>
</entry>
<entry>
<title type="html"><![CDATA[C#的ref和out]]></title>
<id>https://heanrum.github.io/post/cde-ref-he-out/</id>
<link href="https://heanrum.github.io/post/cde-ref-he-out/">
</link>
<updated>2024-09-08T12:33:29.000Z</updated>
<content type="html"><![CDATA[<p>CLR默认所有函数参数默认传值,在C#中无论是值类型还是引用类型的参数都是如此。对于值类型是传递了一个副本。</p>
<pre><code class="language-csharp">sealed class Program
{
static void Main(string[] args)
{
Point p = new Point();
Console.WriteLine(p.ToString());
AddPoint(p);
Console.WriteLine(p.ToString());
}
public static void AddPoint(Point p)
{
p.x += 1;
p.y += 1;
}
public struct Point
{
public int x, y;
public override string ToString() => $"({x}, {y})";
}
}
</code></pre>
<p>上面的代码会输出两个(0,0),因为修改的是传入的副本的值而没有修改对象p本身。</p>
<p>对于引用类型是传递了一个对象的引用,这意味着我们无法改变作为参数传入的那个变量指向的对象。</p>
<pre><code class="language-csharp">sealed class Program
{
static void Main(string[] args)
{
Point p = new Point();
Console.WriteLine(p.ToString());
AddPoint(p);
Console.WriteLine(p.ToString());
}
public static void AddPoint(Point p1)
{
p1 = new Point();
p1.x = 1;
p1.y = 1;
}
public class Point
{
public int x, y;
public override string ToString() => $"({x}, {y})";
}
}
</code></pre>
<p>上面的代码会输出两个(0,0),因为只是修改p1指向的对象,而p指向的对象没有改变。</p>
<p>而通过使用<code>ref</code>和<code>out</code>关键字,我们可以改变上述默认行为,实现参数的引用传递。CLR中是不区分out和ref,无论使用哪个关键字都会生成相同的IL代码。但是C#编译器在处理<code>ref</code>和<code>out</code>时会进行一些额外的检查,以确保参数在使用前被正确赋值。</p>
<pre><code class="language-csharp">sealed class Program
{
static void Main(string[] args)
{
Point p = new Point();
Console.WriteLine(p.ToString());
AddPoint(ref p);
Console.WriteLine(p.ToString());
}
public static void AddPoint(ref Point p1)
{
p1 = new Point();
p1.x = 1;
p1.y = 1;
}
public class Point
{
public int x, y;
public override string ToString() => $"({x}, {y})";
}
}
</code></pre>
<p>上图代码会输出(0,0),(1,1),就是因为ref关键字使得传入了p的引用,因此AddPoint中修改了p1指向的对象也就修改了p指向的对象。</p>
<p>ref和out都能传引用,但是它们有如下的<strong>不同</strong>:</p>
<ul>
<li>ref需要传入一个已经初始化过的对象。</li>
<li>out需要对象在函数内部有被写入过。</li>
</ul>
]]></content>
</entry>
<entry>
<title type="html"><![CDATA[C#的相等判断]]></title>
<id>https://heanrum.github.io/post/cde-xiang-deng-pan-duan/</id>
<link href="https://heanrum.github.io/post/cde-xiang-deng-pan-duan/">
</link>
<updated>2024-09-01T12:11:48.000Z</updated>
<content type="html"><" title="超链接title">官方文档</a>介绍,对于值类型是判断其内部值是否相同,对于引用类型是默认判断是否引用同一对象,对于委托和字符串等也有各自的实现。</p>
<p>值得注意的是,用户定义的struct类型默认没有支持==操作符,但是可以使用ValueType的Equals方法来判断两个值类型的内部值是否相同,例如:</p>
<pre><code class="language-csharp">static void Main(string[] args)
{
Point p1 = new Point(1, 1);
Point p2 = new Point(1, 1);
Console.WriteLine("p1 == p2:{0}", p1.Equals(p2)); // 输出 p1 == p2:True
Console.WriteLine("p1 == p2:{0}", p1 == p2); // 编译错误 Operator '==' cannot be applied to operands of type 'Point' and 'Point'
}
internal struct Point
{
private Int32 m_x, m_y;
public Point(Int32 x, Int32 y)
{
m_x = x;
m_y = y;
}
}
</code></pre>
<p>综上,C#提供了多种方式来判断对象的相等性,包括引用相等和值相等,开发者需要根据具体需求选择合适的方法进行判断。</p>
<h3 id="如何自定义相等判断">如何自定义相等判断</h3>
<p>我们自定义的类如果需要实现自己的相等判断逻辑,那么可以重载Object的<code>public virtual bool Equals(Object? obj)</code>方法以及重写==操作运算符,如果重写了其中之一,那么推荐也重写另一个相等判断,否则可能由于使用习惯使用了没有重写的相等判断造成bug。</p>
<p>重写时需要注意相等性判断要符合一下特性:</p>
<ul>
<li>自反性。x.Equals(x) 应该返回 true</li>
<li>对称性。x.Equals(y) 应该返回和 y.Equals(x) 相同的结果</li>
<li>可传递。x.Equals(y) 和 y.Equals(z) 都返回 true,那么 x.Equals(z) 也应该返回 true</li>
<li>一致性。x.Equals(y) 多次调用应该返回相同的结果,除非x或y的值发生了变化。</li>
</ul>
<p>如果重写了<code>public virtual bool Equals(Object? obj)</code>方法,那么也应该重写<code>GetHashCode</code>方法,因为相等的对象必须有相同的哈希码,否则以这个类的对象作为key来索引时会导致相同的对象索引到不同的数据,而且编译器也会给出有一个<a href="[超链接地址](https://learn.microsoft.com/en-us/dotnet/csharp/misc/cs0659?f1url=%3FappId%3Droslyn%26k%3Dk(CS0659))" title="超链接title">编译警告</a>。</p>
<p>除了使用继承来的Object的相关方法来判断相等,我们还可以继承IEquatable<T>接口实现自己的Equals方法,该方法和Object的Equals方法类似,但是<strong>类型更安全</strong>,因为它的参数是该类型的变量,而不是Object对象,不需要装箱和拆箱操作。</p>
]]></content>
</entry>
<entry>
<title type="html"><![CDATA[装箱拆箱(boxing and unboxing)]]></title>
<id>https://heanrum.github.io/post/zhuang-xiang-chai-xiang-boxing-and-unboxing/</id>
<link href="https://heanrum.github.io/post/zhuang-xiang-chai-xiang-boxing-and-unboxing/">
</link>
<updated>2024-08-31T02:46:06.000Z</updated>
<content type="html"><![CDATA[<h3 id="1引用类型和值类型">1.引用类型和值类型</h3>
<p>为了理解装箱和拆箱,首先需要了解值类型和引用类型的特点。</p>
<ul>
<li>引用类型:
<ul>
<li>必须从托管堆分配</li>
<li>每个对象有一些额外成员,这些成员必须初始化</li>
<li>对象中其它字节总是为0</li>
<li>从托管堆分配对象,可能强制执行一次垃圾回收</li>
</ul>
</li>
</ul>
<p>从引用类型的特点我们可以知道,如果所有类型都是引用类型,由于内存分配和垃圾回收等原因的存在,那么内存管理和性能开销将会非常大。因此,C#语言提供了值类型来优化。</p>
<ul>
<li>值类型
<ul>
<li>一般在线程栈上分配</li>
<li>变量中不包含指向实例的指针</li>
</ul>
</li>
</ul>
<p>由于值类型对象是在栈上分配,因此值类型对象的分配和回收都是比引用类型对象更高效,因为栈上内存分配和回收只需要移动栈指针即可。C#中提供了许多内置的值类型,如int、float、double等。</p>
<p>C#是通过struct和class关键字来区分值类型和引用类型的,struct定义值类型,class定义引用类型。注意这点与C++不同,C++中struct和class只表示类中成员的默认访问权限是public还是private。C++中指定在栈上还是堆中分配内存是通过初始化变量的方式来确定的,使用new运算符则表示在堆上分配内存。也就是说C#中是<strong>定义类型</strong>的开发者决定在什么地方分配内存,而C++中是<strong>使用类型</strong>的开发者来决定。</p>
<h3 id="2装箱和拆箱">2.装箱和拆箱</h3>
<h4 id="21-装箱boxing">2.1 装箱(boxing)</h4>
<p><strong>装箱就是把值类型转换成引用类型的机制。</strong> 装箱发生的时候,会在托管堆上重新分配内存新建一个对象,值类型的字段会复制到新分配的内存上。因此操作装箱后的对象对原始的值类型对象不会产生影响。</p>
<p>那么什么情况下会用到装箱机制呢,或者什么情况下需要把一个值类型转换成引用类型呢?一种常见的情况是将值类型传递给需要引用类型参数的方法时,例如当一个方法需要一个object类型的参数,而你传递的是一个值类型时,就会发生装箱操作。</p>
<pre><code class="language-c#">class Program
{
static void Main(string[] args)
{
ArrayList arrayList = new ArrayList();
Point p = new Point { X = 10, Y = 20 };
arrayList.Add(p); // 发生装箱,把引用添加到ArrayList中
}
}
public struct Point
{
public int X, Y;
}
</code></pre>
<p>例如上面的代码,ArrayList的Add函数接口如下,它接收的参数类型是Object,是一个引用。因此调用ArrayList的Add方法添加一个值类型对象到ArrayList中时,会发生装箱操作。</p>
<pre><code class="language-c#">public virtual int Add(object? value);
</code></pre>
<h4 id="22-拆箱unboxing">2.2 拆箱(unboxing)</h4>
<p>与装箱对应就是拆箱,<strong>拆箱就是把装箱后的值类型从引用类型转换回原始的值类型。</strong> 注意拆箱操作不要求在内存中复制任何字节,而是获取对象中原始值类型指针的过程。但是往往在拆箱之后会有一次复制的操作把拆箱后的对象赋值给一个值类型对象。</p>
<pre><code class="language-c#">Point p1 = (Point)arrayList[0];
</code></pre>
<p>例如上面代码中的<code>(Point)arrayList[0]</code>就是一个拆箱操作,把<code>arrayList[0]</code>中的引用类型转换回值类型Point。</p>
<h3 id="3注意事项">3.注意事项</h3>
<h4 id="31-减少装箱拆箱">3.1 减少装箱拆箱</h4>
<p>从上述描述我们可以知道,装箱拆箱往往伴随着内存分配和数据拷贝操作,因此编写代码过程中应该注意尽量减少装箱拆箱。</p>
<p>看下面这个例子:</p>
<pre><code class="language-c#">static void Main(string[] args)
{
Int32 a = 5;
Console.WriteLine("{0}, {1}, {2}", a, a, a); // 发生三次装箱
}
</code></pre>
<p>使用ildasm.exe可以看到上述代码生成的IL代码:</p>
<pre><code>.method private hidebysig static void Main(string[] args) cil managed
{
.entrypoint
// 代码大小 33 (0x21)
.maxstack 4
.locals init (int32 V_0)
IL_0000: nop
IL_0001: ldc.i4.5
IL_0002: stloc.0
IL_0003: ldstr "{0}, {1}, {2}"
IL_0008: ldloc.0
IL_0009: box [mscorlib]System.Int32
IL_000e: ldloc.0
IL_000f: box [mscorlib]System.Int32
IL_0014: ldloc.0
IL_0015: box [mscorlib]System.Int32
IL_001a: call void [mscorlib]System.Console::WriteLine(string,
object,
object,
object)
IL_001f: nop
IL_0020: ret
} // end of method Program::Main
</code></pre>
<p>可以看到IL代码中有3次box即装箱操作,而我们又不需要修改这三个不同的装箱对象,因此这里可以提前手动装箱,这样可以减少两次装箱操作。<br>
代码如下:</p>
<pre><code class="language-c#">static void Main(string[] args)
{
Int32 a = 5;
Object o = a;
Console.WriteLine("{0}, {1}, {2}", o, o, o);
}
</code></pre>
<h4 id="32-其它装箱情况">3.2 其它装箱情况</h4>
<ul>
<li>值类型对象没有类型对象指针,但是调用重写的虚方法时不需要装箱,因为值类型是sealed,因此调用的虚方法一定就是重写的虚方法。<strong>而如果调用继承的方法(比如<code>GetType</code>或<code>MemberwiseClone</code>)和没有重写的虚方法时,就需要装箱</strong>。因为这些方法在System.Object中定义,需要接收一个this实参,即指向堆对象的指针。</li>
<li><strong>值类型转型为类型的某个接口时需要装箱</strong>,因为接口变量必须包含对堆对象的引用</li>
</ul>
<h4 id="33-使用接口更改已装箱对象中的字段">3.3 使用接口更改已装箱对象中的字段</h4>
<p>自定义的值类型无法继承其它类,但是可以实现接口,因此如果接口提供了修改内部字段的方法,那么就可以通过把装箱对象转成该接口然后通过该方法来修改内部字段,<strong>但是非常不推荐这种,值类型应该是不可变的</strong>。</p>
<p>代码如下:</p>
<pre><code class="language-c#">namespace HelloWorld
{
sealed class Program
{
static void Main(string[] args)
{
Point p = new Point(1, 1);
Console.WriteLine(p); //(1,1)
p.Change(2, 2);
Console.WriteLine(p); //(2,2)
object o = p;
Console.WriteLine(o); //(2,2)
((Point) o).Change(3, 3);
Console.WriteLine(o); //(2,2)
((IChangeable) o).Change(3, 3);
Console.WriteLine(o); //(3,3)
}
}
}
internal interface IChangeable
{
public void Change(Int32 x, Int32 y);
}
internal struct Point : IChangeable
{
private Int32 m_x, m_y;
public Point(Int32 x, Int32 y)
{
m_x = x;
m_y = y;
}
public void Change(Int32 x, Int32 y)
{
m_x = x;
m_y = y;
}
public override String ToString()
{
return String.Format("({0},{1})", m_x.ToString(), m_y.ToString());
}
}
</code></pre>
<p>值得注意的是,<code> ((Point) o).Change(3, 3);</code>这行代码实际上并不会修改o中的字段,因为这里会先拆箱再装箱,产生一个新的Point对象,修改的是这个新的Point对象的字段,而不是o中的字段。但是通过把o转换成IChangeable接口,然后调用Change方法,就可以修改其内部字段。转化接口的过程没有拆箱,因此修改的就是对象o中的字段。</p>
<h4 id="34-值类型应该是不可变的">3.4 值类型应该是不可变的</h4>
<p>通过3.3中的例子我们可以看到,如果一个值类型中的字段是可变的,我们需要高度关注每个装箱和拆箱过程,避免发生预期之外的错误。如果值类型是不可变的,那么我们就不用过多关心什么时候发生了装箱和拆箱(当然仍然需要关注太多装箱拆箱产生的性能问题)</p>
<p>目前FCL(Framework Class Library)的核心值类型 <strong>(Int32、Int64、Int64、UInt64、Single、Double、Decimal、Boolean等)</strong> 都是不可变的,例如我们修改一个Int32的变量的值并不是修改这个变量的内部值,而是新建了一个Int32对象并赋值给这个变量。</p>
<p>我们可以用如下方式创建一个不可变的类型,如果需要修改内部值,我们通过创建一个新的实例来代替修改:</p>
<pre><code class="language-c#">public struct ImmutablePoint
{
public readonly int X;
public readonly int Y;
public ImmutablePoint(int x, int y)