csharp 语法糖

前言

之前在写一些自定义类的时候, 想让其他类比较方便地访问自定义类中的数据或者方法, 就想到了索引器, 还有一些其他的可以提高自定义类实用性或者提高程序易读性的 csharp 语法, 现在已经用了一段时间了, 写一篇博客, 回忆一下, 总结一下.

判断字符串中是否包含指定字符串究竟是使用 Contains 还是 IndexOf ?

引用自 willingtolove 大佬的博客: 判断字符串中是否包含指定字符串

  • 当不区分大小写时, string.IndexOf 方法的效率明显高于 string.Contains 方法;

  • 当区分大小写时, string.Contains 方法的效率明显高于 string.IndexOf 方法;

  • 如果判断的是中文, 没有大小写之分, string.Contains 方法的效率更高;

string 一个按值类型规则处理的引用类型

  1. string 是引用类型.

  2. string 具有不变性, 一旦 string 被创建, 其中的任何字符都不允许改变.

  3. string 重新赋值时, 会建立一个新的 string 对象, 并使指针指向这个新的 string 对象.

  4. 如果需要多次修改 string 的值, 应该使用 StringBuilder.

const 和 readonly 的区别

const 和 readonly 两者在使用上有些相似, 但其本质却又完全不同.

  1. const 通常叫做: 静态常量 或者 编译时常量, readonly 通常叫做: 动态常量 或者 运行时常量.

  2. const 只能修饰 基元类型, 字符串, 枚举 , 不允许修饰结构体.

    [] const 的值必须在编译阶段被唯一确定, 如果 const 修饰的是引用类型 (string 除外), 则只能被初始化为 null. 又因为 const 的值不可被修改, 所以 null 便没有任何意义, 即 const 修饰引用类型虽然编译器不会报错, 但是没有任何实际意义.

  3. const 使用表达式进行初始化时, 表达式中的所有值都必须能够在编译阶段被唯一确定.

  4. const 修饰的值会隐式使用 static 修饰, 因此只能通过类名访问, 同时 const 不可以显式使用 static 修饰.

  5. const 比 readonly 更高效, 但灵活性差. const 常用于定义永不改变且完全唯一的变量, 如圆周率 π, 真空中光速 с, 普朗克常数 h, 基本电荷 e, 电子静止质量和阿伏伽德罗常数等.

  6. 最重要的一点, const 类似于 C 语言中的 define, 实际是没有内存地址的, 也就是不占用内存, const 会在编译时被它的值所取代.

  7. readonly 通常和 static 搭配使用, 即 static readonly, 以代替灵活性不足的 const.

  8. readonly 只能修饰类变量, 不能修饰方法内的局部变量. const 没有此限制.

  9. const 必须在定义时进行初始化, 且之后不可再次赋值. 而 readonly 在定义时可以进行初始化, 同时在构造函数中也可以进行赋值, 因此当使用不同的构造函数来创建实例时, readonly 的值可能不同, 但除此以外不可再次赋值.

  10. readonly 修饰的引用类型, 只是引用本身不可被修改, 其内部的成员变量依旧可以被修改!

索引器

索引器语法可以让自定义类像数组一样, 可以使用索引来直接访问其中的数据, 这样外部类在取用我们内部数据的时候就很方便了.

如何在自定义类中实现索引器

  1. csharp 中需要使用 gettersetter 来实现索引器.

  2. 索引器的名称是固定的, 只能是 this.

  3. 建立索引器时, 访问限制通常都设置为 public, 虽然语法上并没有任何限制, 但是除非是仅内部使用, 否则这个索引器没有任何意义~

  4. 索引器也具有返回值和参数值, 返回值和方法的写法是完全一致的, 但是索引器的参数是使用方括号 [] 包起来的, 这一点要注意!

  5. 索引器的返回值和参数值并没有特殊要求, 不仅仅可以是 int 类型, 其他类型也可以.

  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
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
using System;

namespace IndexerTest
{
public struct Vector2
{
public int x, y;

public Vector2(int x, int y)
{
this.x = x;
this.y = y;
}
}

public enum EM_Direction
{
Up, Down, Left, Right
}

class Program
{
static void Main()
{
IndexerDirection indexer = new IndexerDirection();

// 需要依次操作四个方向时, 可以直接使用 for 循环, 索引为 int 类型
for (int i = 0; i < 4; i++)
{
Console.WriteLine(indexer[i].x + ", " + indexer[i].y);
}

// 单独操作特定方向时, 可以使用 枚举 作为索引
Console.WriteLine(indexer[EM_Direction.Up].x + ", " + indexer[EM_Direction.Up].y);

Console.ReadKey();
}
}

/// <summary>
/// 方向索引器
/// </summary>
public class IndexerDirection
{
private Vector2 up = new Vector2(0, 1);
private Vector2 down = new Vector2(0, -1);
private Vector2 left = new Vector2(-1, 0);
private Vector2 right = new Vector2(1, 0);

public Vector2 this[EM_Direction index]
{
get
{
switch (index)
{
case EM_Direction.Up:
return up;
case EM_Direction.Down:
return down;
case EM_Direction.Left:
return left;
case EM_Direction.Right:
return right;
default:
return up;
}
}
set
{
switch (index)
{
case EM_Direction.Up:
up = value;
break;
case EM_Direction.Down:
down = value;
break;
case EM_Direction.Left:
left = value;
break;
case EM_Direction.Right:
right = value;
break;
default:
up = value;
break;
}
}
}
public Vector2 this[int index]
{
get
{
switch (index)
{
case 0:
return up;
case 1:
return down;
case 2:
return left;
case 3:
return right;
default:
throw new Exception("下标越界!");
}
}
set
{
switch (index)
{
case 0:
up = value;
break;
case 1:
down = value;
break;
case 2:
left = value;
break;
case 3:
right = value;
break;
default:
throw new Exception("下标越界!");
}
}
}
}
}

别名

这个语法我在另一篇博客 委托(一) 委托的基本用法 的代码块中用过, 没想到长时间不用, 便忘得一干二净了...🙄

别名的语法规则

  1. 起别名的格式为: " using 新名称 = 类型; ".

  2. 在命名空间外进行重命名时, 必须写被重命名类型的完整路径.

  3. 在命名空间内进行重命名时, 可以使用 using 引用简化路径.

  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
28
29
30
31
32
using System;
using System.Collections.Generic;
using NumberDic = System.Collections.Generic.Dictionary<int, int>; // 将 int int 类型的字典重命名为 NumberDic

namespace RenameTest
{
using StringDic = Dictionary<string, string>; // 将 string string 类型的字典重命名为 StringDic

class Program
{
//using CharDic = Dictionary<char, char>; // 写在这里将编译错误

static void Main()
{
// 定义一个 NumberDic 实例
NumberDic numberDic = new NumberDic
{
{ 1, 10000 }
};

// 定义一个 StringDic 实例
StringDic stringDic = new StringDic
{
{ "1", "10000" }
};

Console.WriteLine(numberDic[1]);
Console.WriteLine(stringDic["1"]);
Console.ReadKey();
}
}
}

运算符重载

在自定义类中实现常用运算符的重载后, 外部进行变量间运算时就很方便了. 最常见的重载运算符就是相等运算符 == 了.

运算符重载的语法规则

  1. "重载" 一词便决定了只能重写已有的运算符, 不能自创运算符.

  2. 并不是所有的运算符都可以重载, 如赋值运算符不能被重载.

  3. 必须使用 public static 返回值类型 operator 运算符 () { } 的格式来重载运算符.

  4. 参数的数量必须和运算符原本的参数数量一致.

  5. 某些运算符必须成对重载. 像大于 '>' 和小于 '<'.

运算符重载实例

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
using System;

namespace OperatorTest
{
class Program
{
static void Main()
{
Player a = new Player()
{
id = "001",
name = "Kirito",
email = "Kirito@Kirito.com",
level = 78,
hp = 170000,
mp = 8000
};
Player b = new Player()
{
id = "002",
name = "Asuna",
email = "Asuna@Asuna.com",
level = 76,
hp = 150000,
mp = 170000
};

Player c = a + b;
Console.WriteLine(c.id);
Console.WriteLine(c.name);
Console.WriteLine(c.email);
Console.WriteLine(c.level);
Console.WriteLine(c.hp);
Console.WriteLine(c.mp);
Console.ReadKey();
}
}

public struct Player
{
public string id;
public string name;
public string email;
public int level;
public int hp;
public int mp;

// 重载运算符 +
public static Player operator + (Player a, Player b)
{
Player player = new Player
{
id = a.id,
name = a.name,
email = a.email,
level = a.level + b.level,
hp = a.hp + b.hp,
mp = a.mp + b.mp
};

return player;
}

// 重载运算符 -
public static Player operator - (Player a, Player b)
{
Player player = new Player
{
id = a.id,
name = a.name,
email = a.email,
level = a.level - b.level,
hp = a.hp - b.hp,
mp = a.mp - b.mp
};

return player;
}
}
}

? 和 ??

? 可空 (Nullable) 类型

如果编程时有一种特殊的需求, 比如接收一个返回值, 这个返回值在运算有效时返回 int 类型, 但是在运算无效时返回 null 类型, 此时就可以使用 csharp 中的 Nullable 类型来接收. 单问号就是做这个的.

在 int, double, bool 等无法直接赋值为 null 的数据类型后面加一个 ? 所定义出来的变量便可以赋值为 null.

Nullable 类型变量的定义

? 可以紧跟在类型后面, 也可以不紧跟.

1
2
3
4
int ? intNullable = null;
bool ? boolNullable = null;
float? floatNullable = null;
double? doubleNullable = null;

?? 为空判断

csharp 提供了一个双问号运算符来判断表达式是否为空, 如果为空则返回 ?? 后面的值, 如果不为空, 则返回自身.

?? 的使用

下方代码段中, 如果 intNullable 为 null, 则打印数字 0, 不是 null, 则打印 intNullable 自身.

1
2
int? intNullable = null;
Console.WriteLine(intNullable ?? 0);

预处理指令

预处理指令编写的逻辑是, 让编译器在正式编译代码之前, 对代码进行处理.

语法规则

  1. 预处理指令必须以 # 开头.

  2. 预处理指令必须位于行首.

  3. 预处理执行不是语句, 不以分号 ';' 结尾.

常用预处理指令

符号定义

预处理指令 作用
#define 定义符号
#undef 取消定义符号

条件分支

预处理指令 作用
#if 条件指令的开始符号, 判断特定符号是否被定义
#elif 创建复合条件指令
#else 创建复合条件指令
#endif 条件指令的结束符号

信息输出

预处理指令 作用
#warning 从代码的指定位置生成一个警告
#error 从代码的指定位置生成一个错误

大纲管理

预处理指令 作用
#region 指定一个可折叠的代码块
#endregion 标识 #region 结束

信息控制

预处理指令 作用
#line 修改编译器输出错误和警告的行数, 文件名
#pragma 抑制或还原指定的编译警告
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
using System;

namespace Exercise
{
#line 200 "boss.cs"
#warning 这里其实是 Program.cs 文件的第 6 行!
#line default
#error 这里其实是 Program.cs 文件的第 8 行!

class Program
{
static void Main()
{
Console.ReadKey();
}
}
}

比如上面的代码段在编译时产生的警告和错误信息是:

line 命令

可以看到警告信息的文件名被修改为了 boss.cs, 行号变成了 200; 而错误信息的文件名和行号都是正确的.

参考文章

  • C# 运算符重载

  • C# 预处理指令

  • const与readonly的区别