C#编程中的动态类型与位运算符详解
1. 反射与委托的应用
在编程中,
hasStuff
的值会被设置为独特的值,接着会进行委托的赋值操作。委托的值会与类的字段进行名称检查,当找到匹配的字符串名称时,这些值会被添加到一个数组中。之后,会使用填充了类属性的对象数组来调用委托。
通过反射,类能够识别参数并从自身成员中为函数赋值。将此功能添加到传入的委托中,就可以为游戏角色或对象分配各种修改。
2. C#中的动态类型
C#通常是一种强类型语言,但像
var
类型,存储在变量中的数据类型并非总是明确定义的。
var
类型在接收值后才会被定义,这在赋值的值可能会改变的情况下很有用,比如在使用 Linq 值时。而
dynamic
类型则有所不同。
在
Dynamic_Scene
中,找到同名游戏对象上附加的
Dynamic
脚本。
dynamic
类型与
var
不同,它可以随时重新赋值为任何类型,这违背了强类型语言的定义。特别是
GetType()
函数不会给出变量存储类型未设置的线索。
dynamic
类型并不常用,为了代码的清晰性,也不应频繁使用。定义类型的做法很重要,类型的定义和使用可以防止变量意外改变。即使是泛型类型
<T>
,在赋值后也会成为一个固定类型,之后类型就不能再改变。
变量在大多数情况下,一旦被赋值或创建就会保持静态或不变,但
dynamic
类型可以改变。这可能会引入一些混乱,导致 Visual Studio 原本能够捕获的错误被忽略,因为动态变量会跳过编译时的错误检查,所以使用时要谨慎。
在某些情况下,即使没有特定的赋值操作,
dynamic
类型也能改变类型。例如,将一个
int
除以一个
float
,类型会被重新赋值,而且这种类型突变不会产生错误或警告,这种行为类似于 LUA 或 Python 等语言,变量的类型会随着赋值的改变而改变。
当以意外的方式使用
dynamic
变量时,就会出现问题。存储在
dynamic
变量中的数据仍然作为特定类型起作用,所以像
++
这样的操作不能用于字符串。不过在大多数情况下,这类错误会在游戏运行前被 Visual Studio 捕获。
3. ExpandoObject
dynamic
类型独特的类型更改能力可以进一步扩展。
System.Dynamic
命名空间包含一个名为
ExpandoObject
的特殊对象。
通过点运算符,新的
ExpandoObject()
可以动态地创建、添加名称并定义成员的类型。但使用
ExpandoObject()
的强大功能时要谨慎,因为一旦赋值,在使用之前无法调试
ExpandoObject
每个成员的类型和功能。
ExpandoObject
可以引用自身,例如在分配给
expando.gizmo
对象的
Action
中就能看到。而且
ExpandoObject
中的值可以是某种委托,而不仅仅是普通的数据类型,它还可以用作不同的类型。
4. Expando Reader
数据通常以文本形式存储,游戏对象的属性需要从字符串值解析为预期的数据类型,以便在游戏场景的其他地方使用。例如,一个僵尸可能有名称、生命值和世界中的位置,这些分别存储为字符串、整数和
Vector3
类型,但在文本形式中只有字符串类型。
数据可以来自任何地方,这里我们从用
\n
字符分隔并用
+
字符组合的几行数据开始。由于之前已经介绍过读写文本文件到磁盘的操作,这里就不再赘述。
为了将
ExpandoObject
转换为可以动态添加新成员的类型,我们可以将
ExpandoObject()
转换为
IDictionary<string, object>
。这样就可以使用字典的接口成员
Add()
来创建新成员。
数据字符串用
\n
字符分割后会变成三个字符串,例如
Name:Rob
、
HP:10
和
Position:1.23,4.56,7.89
。然后这些字符串再用
:
字符分割成字符串对象键值对,每个键值对都会添加到
expandoAsDictionary
变量中。
接着,将
expandoAsDictionary
赋值给
expando
,将字典转换回
ExpandoObject
。此时,
expando
对象就有了
Name
、
HP
和
Position
属性。对于
Name
键值对,不需要转换值,所以
expando.Name
就是字符串
Rob
。
ExpandoObject
对象的值也是动态的,所以字符串
"10"
可以通过解析转换为整数,应用
Parse
函数后,
expando.HP
就变成了整数
10
。
expando.Position
需要通过分割和更多的解析操作来转换,这也表明
dynamic
类型可以被赋值为 Unity 中的类型。
这里给出的示例没有进行错误检查,不能保证从磁盘读取的文本文件不会包含意外字符或多余的空格。但有趣的是,无需进行复杂的转换或类型分配,就可以将字符串转换为整数,
dynamic
类型很适合这种情况。
5. 动态类型的用途
dynamic
类型被添加到 C# 中是为了与 LUA 或 Python 等语言兼容。在 LUA 中,变量只是名称,唯一的定义是其作用域,分为局部或全局。Python 也是如此,简单地写
“bob = 10”
,
bob
就成为了值为
10
的变量。
使用
dynamic
类型可以以类似的方式使用变量,但这也会带来更多意外行为的可能性,并且缺乏足够的保护机制。
dynamic
类型在解析更复杂的类型(如 Json 或 XML)时也很有用。在 C# 用于 Web 服务器时,
dynamic
常用于读取和构建复杂的 HTML 网页,每个
<ELEMENT>
都可以成为一个带有值的新
expando.object
。
而且,
ExpandoObject
中的每个对象都可以有带值的成员,这些成员还可以有更多的成员和值,
ExpandoObject
名副其实,功能非常强大。
6. 位运算符基础
在学习计数时,我们学习的是十进制系统,它是一种以 10 为基数的数字系统,但还有许多其他的数字系统。计算机使用二进制或基数为 2 的数字系统,计算机中的晶体管只有两种状态:开启或关闭,更准确地说,是处于充电状态或接地状态。
对于常见的游戏开发任务来说,操作单个位似乎是比较底层的操作,但一旦掌握了翻转单个位的技巧,就可以实现很多不同的功能。下面回顾一下数字在计算机内存中的存储方式。
一个字节由八个
1
和
0
组成,二进制
0000 0000
表示十进制的
0
,这就是存储在计算机内存中的二进制表示形式。十进制的
1
存储为二进制
1000 0000
,十进制的
2
表示为
0100 0000
,第二位代表十进制的
2
,所以十进制的
128
是二进制
0000 0001
。
7. 字节序:大端和小端
我们看到的数字排列方式称为小端字节序。这个名称源于二进制中最大的值存储在
1
和
0
序列的末尾,由于最低值存储在数字的开头,所以称为小端字节序。如果将
128
用二进制表示为
10000000
,即最大的值先存储,那么这就是大端字节序的数字。
每个二进制位的值都是 2 的幂次方,第一位要么是
0
要么是
1
,即
0
或
1
的 0 次方;第二位要么是
0
要么是
2
的 1 次方,即
2
;第三位是
0
或
2
的 2 次方,即
4
,以此类推,二进制数的每个位都遵循这个模式。
为了说明二进制的工作原理以及
1
和
0
所代表的含义,考虑二进制由
1
和
0
组成,如果每个位代表 2 的幂次方,那么一个字节中的每个位分别代表
1
、
2
、
4
、
8
、
16
、
32
、
64
和
128
,这样就可以表示
0
到
255
之间的每个数字。
以一个 2 位的数字为例,要数到
3
,我们从
00
开始,然后是
10
、
01
,最后是
11
。第一位是
0
或
1
,第二位是
0
或
2
。数
0
时用
00
,组合
10
表示
1
,数到
2
用
01
,最后
3
由
10
和
01
相加得到
11
,即
1 + 2 = 3
,这就是计算机的计数方式。
这种排列方式源于与早期处理器的向后兼容性,比如 1972 年英特尔制造的 8008 处理器,这是一个小型处理器,使用小端字节存储方式以便更轻松地与串行总线(一种在系统之间传输字节的计算机接口)进行通信,这就要求先计算较低的值。
如今,大多数现代 CPU 采用大端字节序,因为作为人类,我们习惯先读取最大的值,而不是最后读取。例如,“一百万一百一十一万一千一百一十一” 是从大值开始,逐渐到小值,现代汇编语言也是按照这种方式组织的。
8. 有符号和无符号数
上述系统同样适用于由八个
1
和
0
组成的字节。一个
int
类型存储 32 个
1
和
0
,但有一个区别,“有符号” 和 “无符号” 表示一个数字是否允许有负值。如果是有符号数,就会用一个位来表示正负号,虽然通常在正数前面不会看到
+
号,但它是隐含的。
回到 2 位数字的例子,我们仍然只能数到三个值。从
10
开始,它表示
-1
,当第一位代表值时,第二位表示符号。可以用
00
表示
0
,
11
表示
+1
,这样仍然有三个可用的值:
-1
、
0
和
+1
。是否使用一个位来表示正负号就称为有符号或无符号。当只有正值时,我们认为是无符号数;如果可以表示正负值,程序员称之为有符号数。
有符号字节或
sbyte
的范围是从
-127
到
+127
,使用小端字节序时,如果值为负,最后一位是
1
,如果为正,最后一位是
0
。例如
-1
会有很多
1
,这是因为对于负数,我们改变了位值的使用方式,这个系统更像是从
128
中减去某个值来得到有符号字节中的值。
一个
int
类型的范围是从
-2,147,483,648
到
2,147,483,647
,注意负数比正数多一个,稍后会解释原因。无符号
int
或
uint
的范围是从
0
到
4,294,967,295
,这些数字的符号由最后一位控制。
常规的数学运算符按预期工作,但当达到这些数字的极限时,会出现有趣的现象。通常,可能期望
c
输出
2147483648
,但实际上得到的是
-2147483648
,这是一个负数。还有无符号版本的
int
即
uint
,其中
u
表示无符号。
当无符号
int
从
4294967295
加
1
时,结果会变为
0
。当
umax
被赋值为
4294967295
时,可以想象 32 位的数字被 32 个
1
填满,当加
1
时,这些位会溢出,结果变成 32 个
0
。所以,计算机中的数字因为是二进制的,表现会很奇怪,这是计算机运算中一个简单但有点棘手的事实。
以一个 4 位的数字(有时称为半字节)
0000
为例,它由四个数字组成。加
1
后得到
0001
,再左移一位;再加
1
得到
0010
,表示
2
,记住第二位代表
2^1
。
再加
1
得到
0011
,即
1 + 2
或
2^0 + 2^1
,结果是
3
。再加
1
得到
0100
,前两位重置,
1
再次左移,结果是
4
。再加
1
得到
0101
,即
1 + 4
。继续加
1
得到
0110
,再加
1
得到
0111
,即
1 + 2 + 4
,结果是
7
。
最后再加
1
得到
1000
,这是表示
8
的最后一位。这个过程会一直持续,直到得到
1111
,按照加
1
的模式,
1
会右移,但没有空间了,所以结果变成
0000
,4 位数字溢出了。4 位数字的范围是从
0
到
15
,如果是有符号的 4 位数字,范围是从
-7
到
7
。
这种用
2^0 + 2^1 + 2^2 + ...
进行计数的系统是 1679 年由数学家戈特弗里德·威廉·莱布尼茨发明的,他还发明了微积分,如果在数学课上遇到困难,可以“责怪” 莱布尼茨。但如果没有他,我们就不会有计算机,他在创建这个系统后说:“当数字简化为
0
和
1
时,美丽的秩序无处不在。” 确实,计算机带来了许多美好的事物。
这个系统也适用于更大的数字,对于 32 位的数字,在溢出之前可以数到
4294967296
。使用有符号数时,会用第一位或最后一位来存储符号(
+
或
-
),严格来说,这是一个带符号的 31 位数字,范围是
+2^31 - 1
(因为要保留一个
0
)或
-2^31
,即
2147483647
到
-2147483648
。虽然计算机受每个数字的位数限制,但只要记住这些限制,我们仍然可以处理这些数字。
9. 按位或运算符
|
位运算符
|
、
&
、
^
和
~
用于在位级别操作数字,而不是在数学级别操作。这里的 “Bar”(
|
) 运算符,通常认为
1 + 2 = 3
,也可以用
1 | 2 = 3
,但它的工作方式可能与想象的不同,
|
运算符用于合并位。
以之前的 4 位数字为例,在 C# 中可以使用如下表示方式:
// 示例代码
int result = 1 | 2; // 结果为 3
上述示例中,会查看每个位,如果其中一个位是
1
,则结果位设置为
1
,如果两个位都是
1
,结果仍然是
1
。所以在上述示例中,结果是
7
,而不是可能想象的
11
。
乍一看,可能不会立即看到按位使用数字的优势,但程序员喜欢以独特的方式思考,学习编写代码就是要像程序员一样思考,这从来都不是一件容易的事情。
10. 枚举和数字
如果使用枚举,可以为每个值设置一个数字。例如:
enum CharacterClasses
{
Farmer = 0,
Fighter = 1,
Thief = 2,
Wizard = 4,
Archer = 8
}
可以看到这些值被分配了与二进制计数相同的值。所以,如果
0000
代表
Farmer
,那么
0001
代表
Fighter
,
0010
代表
Thief
,
0100
代表
Wizard
,
1000
代表
Archer
。如果想要一个既是
Fighter
又是
Wizard
的多职业角色,该怎么办呢?
可以使用
multiClass = fighter | wizard;
语句将两个值合并到枚举中。在多职业值的内部,通过
0001 | 0100
合并位后得到
01010000
。这可能看起来很奇怪,因为结果是
5
,而
CharacterClasses
枚举中没有分配为
5
的数值,但这是因为我们将枚举用作位标志,而不是普通的数字。
11. 按位与运算符
&
为了找出哪些位正在使用,我们使用按位与运算符
&
。通过
&
运算符可以比较两组位,查看它们是否匹配。
// 示例代码
int multiClass = (int)CharacterClasses.Fighter | (int)CharacterClasses.Wizard;
bool isFighter = (multiClass & (int)CharacterClasses.Fighter) != 0;
bool isWizard = (multiClass & (int)CharacterClasses.Wizard) != 0;
bool isArcher = (multiClass & (int)CharacterClasses.Archer) != 0;
上述代码中,
&
运算符只有在两个值中的位都为
1
时才会返回
1
。使用这个运算符可以检查角色属于哪些职业。例如,多职业角色在字节的第一位和第三位有
1
,使用
& CharacterClasses.Fighter
可以检查
multiClass
在第一位是否有值,同样可以检查
multiClass
在第三位是否有值。检查
multiClass
是否为
Archer
时,使用
& CharacterClasses.Archer
会发现没有匹配的位。
12. 按位异或运算符
^
还可以使用按位异或运算符
^
来查看哪些位不匹配。
// 示例代码
int value1 = 0b0101;
int value2 = 0b0110;
int result = value1 ^ value2; // 结果为 0b0011
在上述代码中,
^
运算符会显示两组位中不匹配的位置,
0
会被忽略,如果有
1
则会采取相应操作。在这个例子中,第一位和第二位不匹配,后面的位相同,所以结果中这些位为
0
。
回到之前使用的枚举,有一个多职业的
Fighter
和
Wizard
角色,如果想从枚举中移除
Fighter
,就可以使用异或运算符。
MultiClass
是位的组合,通过
multiClass ^= (int)CharacterClasses.Fighter;
就可以移除
Fighter
职业。
综上所述,C# 中的动态类型和位运算符为开发者提供了强大的工具,但在使用时需要谨慎考虑其特性和潜在的问题。动态类型可以增加代码的灵活性,但可能会引入难以调试的错误;位运算符虽然是底层操作,但在某些场景下能实现高效的功能。开发者应根据具体的需求和场景合理运用这些特性,以实现更优质的代码。
C#编程中的动态类型与位运算符详解
13. 按位取反运算符
~
按位取反运算符
~
用于反转一个数的所有位。也就是说,它会将二进制数中的每一个
0
变为
1
,每一个
1
变为
0
。
// 示例代码
int number = 5; // 二进制表示为 0000 0101
int result = ~number; // 结果为 -6,二进制补码表示
在上述代码中,
number
的二进制表示是
0000 0101
,使用
~
运算符后,所有位都被反转,得到
1111 1010
。在计算机中,负数通常以补码形式存储,所以
1111 1010
表示的是
-6
。
按位取反运算符在一些特定的场景中非常有用,比如在需要反转标志位或者进行掩码操作时。
14. 位运算符的应用场景
位运算符在很多实际场景中都有广泛的应用,以下是一些常见的应用场景:
- 标志位管理 :在游戏开发中,经常会使用标志位来表示角色的各种状态,比如是否无敌、是否隐身等。可以使用位运算符来高效地管理这些标志位。
// 定义标志位枚举
[Flags]
enum CharacterFlags
{
None = 0,
Invincible = 1,
Invisible = 2,
Flying = 4
}
// 设置标志位
CharacterFlags flags = CharacterFlags.Invincible | CharacterFlags.Flying;
// 检查标志位
bool isInvincible = (flags & CharacterFlags.Invincible) != 0;
bool isFlying = (flags & CharacterFlags.Flying) != 0;
// 移除标志位
flags &= ~CharacterFlags.Invincible;
- 数据压缩 :在需要存储大量布尔值或者小整数的场景中,可以使用位运算符将多个值压缩到一个整数中,从而节省内存空间。
// 假设需要存储 4 个布尔值
bool flag1 = true;
bool flag2 = false;
bool flag3 = true;
bool flag4 = false;
// 将布尔值压缩到一个整数中
int compressedData = (flag1 ? 1 : 0) | ((flag2 ? 1 : 0) << 1) | ((flag3 ? 1 : 0) << 2) | ((flag4 ? 1 : 0) << 3);
// 从压缩数据中提取布尔值
bool extractedFlag1 = (compressedData & 1) != 0;
bool extractedFlag2 = ((compressedData >> 1) & 1) != 0;
bool extractedFlag3 = ((compressedData >> 2) & 1) != 0;
bool extractedFlag4 = ((compressedData >> 3) & 1) != 0;
- 图像处理 :在图像处理中,位运算符可以用于颜色通道的分离、合并和修改。例如,可以使用按位与运算符提取特定颜色通道的值,使用按位或运算符合并多个颜色通道。
15. 位运算符的性能考虑
位运算符是非常底层的操作,通常比普通的数学运算符和逻辑运算符更快。这是因为位运算符直接操作二进制位,不需要进行复杂的数学计算或者逻辑判断。
然而,在使用位运算符时,也需要注意一些性能方面的问题:
-
可读性 :位运算符的代码通常比较难以理解,尤其是对于不熟悉位操作的开发者来说。因此,在编写代码时,应该尽量添加注释来解释代码的意图,提高代码的可读性。
-
溢出问题 :在进行位运算时,需要注意溢出问题。例如,在使用左移运算符
<<时,如果移动的位数超过了数据类型的位数,可能会导致数据丢失或者产生意外的结果。
16. 动态类型与位运算符的结合使用
在某些情况下,可以将动态类型和位运算符结合使用,以实现更加灵活和高效的代码。
例如,在处理动态数据时,可以使用位运算符来对数据进行加密或者压缩,然后使用动态类型来存储和处理这些数据。
dynamic data = 10;
int mask = 3; // 二进制表示为 0011
// 使用位运算符对数据进行处理
dynamic result = data & mask;
在上述代码中,首先定义了一个动态类型的变量
data
,然后使用位与运算符
&
对
data
和
mask
进行操作,最后将结果存储在动态类型的变量
result
中。
17. 总结
通过对 C# 中动态类型和位运算符的详细介绍,我们了解到:
-
动态类型 :
dynamic类型为 C# 带来了与其他动态语言类似的灵活性,使得代码可以在运行时动态地处理不同类型的数据。它在处理复杂数据结构(如 JSON、XML)、与动态语言交互以及构建灵活的代码逻辑时非常有用。但同时,动态类型也会增加代码的不确定性,可能导致运行时错误,因此需要谨慎使用。 -
位运算符 :位运算符提供了一种底层的操作方式,可以直接对二进制位进行操作。它们在标志位管理、数据压缩、图像处理等场景中具有高效性和独特的优势。然而,位运算符的代码通常较难理解,需要开发者具备一定的二进制知识和编程技巧。
在实际的编程过程中,开发者应该根据具体的需求和场景,合理地选择使用动态类型和位运算符。对于需要高度灵活性和动态性的场景,可以考虑使用动态类型;而对于需要高效处理二进制数据的场景,则可以充分发挥位运算符的优势。同时,要注意代码的可读性和可维护性,避免引入难以调试的错误。
为了更清晰地展示动态类型和位运算符的特点和应用,以下是一个对比表格:
| 特性 | 动态类型 | 位运算符 |
| — | — | — |
| 灵活性 | 高,可在运行时处理不同类型数据 | 低,主要处理二进制位 |
| 性能 | 相对较低,有运行时开销 | 高,直接操作二进制位 |
| 可读性 | 一般,可能导致代码难以理解 | 低,需要二进制知识 |
| 应用场景 | 处理复杂数据结构、与动态语言交互 | 标志位管理、数据压缩、图像处理 |
下面是一个简单的 mermaid 流程图,展示了在选择使用动态类型和位运算符时的决策过程:
graph TD;
A[需求场景] --> B{是否需要高度灵活性?};
B -- 是 --> C[考虑使用动态类型];
B -- 否 --> D{是否需要处理二进制数据?};
D -- 是 --> E[考虑使用位运算符];
D -- 否 --> F[使用常规类型和运算符];
通过合理运用动态类型和位运算符,开发者可以编写出更加高效、灵活和强大的 C# 代码。
超级会员免费看

被折叠的 条评论
为什么被折叠?



