让我们先磨一磨剑,一起回顾必须了解的用 C# 和 .NET 8.0 投入开发前的准备知识。我想根据大部分读者的知识储备,这个章节的很多内容可能您会觉得非常熟悉,特别是用过 C/C++ 的码农,您完全可以快速浏览,甚至跳过熟悉的内容。C# 既然是一个快速开发的程序语言,学习的过程当然也要符合这个语言的核心哲学之一:快速。并且,这章的很多内容会很简略,我们假设读者都会知道基本的编程语言知识,所以这章的内容主要是整理一些特别的要点。
我们推荐的学习方法是“多用”,像初中数学老师说的,用多了,慢慢就理解了。所以,我们也废话少说,马上进入正题。
本地变量
在 C# 中,本地变量,除了一般的变量,也包括对象的字段和数组的元素。C# 是静态类型语言,定义的元素,函数等都要先编译再运行,所以定义时要指定变量类型。变量名可以是字母和下划线,应用“驼峰”规则。
C# 可以用 var
声明隐性类型的变量,但是这并不等价于像 Javascript 的动态类型,隐性类型的意思是编译器会辨认出来是什么具体类型。比如:
List nameList1 = new();
var nameList2 = new List(); //Implicit declare
我们推荐用 var
声明变量,可以让代码看起来更加整洁。
作用域
不只变量有作用域,方法、属性、类型等等,几乎任何有名称定义的东西都有作用域。一般说来,元素的作用域就是它所在的代码块,用 {}
限定。请看例子:
int theValue = GetAValue();
if (theValue > 10)
{
int minusTen = theValue - 10;
}
Console.WriteLine(minusTen); // Compiler Error!
最后的句子会产生编译错误,因为变量 minusTen 作用域只在选择语句限定的代码块内,不能被控制台的 WriteLine() 方法访问。
防止变量名模糊
变量名模糊是指一个名字不能指示超过一个元素,即使它们的作用域没有冲突。请看例子:
int oneValue = GetAValue();
if (oneValue > 0)
{
int anotherValue = oneValue - 10; // Compiler Error!
Console.WriteLine(anotherValue);
}
int anotherValue = 100;
因为嵌套的代码块声明了一个和外面代码块一样名字的变量,虽然它们不在同一作用域,但是在同一声明空间,比如:在同一个方法里面,这种情况就会有变量名模糊,编译会出错。
好消息是现代的集成开发环境(IDE)和类似 Visual Studio Code 这样的类 IDE 在安装好 C# 扩展后,这类错误在代码编辑的过程中,就会被 IDE 报错。所以呢,这个知识点讲了半天都是废话,我们以后注意避免。 LOL!
表达式
表达式就是操作符和运算符以及它们的操作对象组合起来的序列。比如:
int[] numbers = new int[100];
for (int i = 1; i < 101; i++)
{
numbers[i-1] = i;
}
int sn = 100 * ( numbers[0] + numbers[99] ) / 2; // expression
Console.WriteLine($"sum is {sn}");
这个例子的 sn 等号后面就是一个表达式。总体说来,表达式不只有结果,还要有效果!如果我们的语句就像这样:
Console.Write("Hello World!");
Math.Round(9.99);
Console.WriteLine(result = 100);
这段代码的中间那句语句有结果,但是没有效果,就不是一个有效的表达式,因为有或者没有这句语句,对代码的执行结果不会带来任何改变,就是没有效果。有些编译器会觉得我们在说废话,给我们报错。
编译符号
.NET SDK 定义了支持两种配置的编译符号:Debug 和 Release,还有一类 TRACE 的符号两种配置适用。还有项目目标对应的符号,暂不讨论。
编译符号一般由 #if, #else,#elif 和 #endif 连接起来。请看下面的条件编译配置:
#if DEBUG
Console.WriteLine("Starting some job ...");
#endif
DoSomeJob();
#if DEBUG
Console.WriteLine("Job Done!");
#endif
几乎在任何语言的程序调试中,我们都经常会用到类似的代码。
C# 作为现代快速开发工具,为我们提供了一个简洁的快速的机制实现上面的过程,叫“条件方法”,请看例子:
[System.Diagnostics.Conditional("DEBUG")]
static void DisplayDebugInfo(object obj)
{
Console.WriteLine(obj);
}
我们的代码如果调用了加了这个附注属性的方法,也可以一样有采用条件编译符号后得到的效果。如果以后在生产版本中没有这个输出调试提示的必要,可以删掉属性。
#error 和 #warning
刻意报错,防止在错误的路上走得更远。比如,我们可以检查应用是否支持当前平台:
#if WINDOWS
Console.WriteLine("Support!");
#elif LINUX
#error "Linux would be supported soon, but not now!"
#else
#error "unsupported platform!"
#endif
#line
主要用于报告有关错误,调试或者跟踪信息。经常用于代码生成或错误处理的情况。请看例子:
#line 100 "TargetFile.cs"
void OtherMethod()
{
int x = "hello"; // 编译器会报错,位于 TargetFile.cs 的 100 行的位置
}
这种方式有助于让代码生成器整合众多文件到一个文件。
我们可以用 #line hidden 让 debugger 忽略非关键一些语句,以简化调试过程:
void SomeMethod()
{
Console.WriteLine("This line is visible.");
#line hidden
Console.WriteLine("hidden from dibugger.");
int hiddenValue = 100;
#line default
}
#pragma
一般来说,我们用它来隐藏编译警告;一般来说,我们要尽量避免用到这个功能。
隐藏多重警告的例子:
#pragma warning disable CS0168, CS0219
void SomeMethod()
{
int unusedVar;
int assignedButNeverUsed = 100;
}
#pragma warning restore CS0168, CS0219
检测编译器指定平台的例子:
#if NET6_0
#pragma warning disable CS8602 // 取消可空 nullability 警告
#endif
其他编译符号
#nullable 允许控制可空元素内容和警告信息。
#region 和 #endregion 被用来让文本编辑器可以辨认指定区域的文本。
基本数据类型
除了一般的数据类型如数字类型,布尔类型,字符,字符串外,C# 的基本数据类型还包括元组(Tuples),动态类型(Dynamic)和对象(Object)。
基本类型中有些是 C# 本身定义的,有些是属于特定的运行时库(runtime library),我们在写代码的过程要留意,写到后面有几万行代码的时候,可能一下子都找不到类型定义错误的原因。
我们就不一一赘述那些和其他语言相同的概念,就举一些重点回顾一下:
- 数字类型之间不提供隐性转换,可以显式转换:
int eight = 8;
int willFail = 7.0; //Error
int willFailToo = eight / 1.0; //Error
int willSucceed = (int) (eight / 1.0)
- C# 定义了一个
checked
关键字,用来检查变量或代码块中的数字类型溢出,程序更健壮。
try
{
int max = int.MaxValue;
Console.WriteLine($"Max Value: {max}");
// This addition causes an overflow
int result = checked(max + 1); // Will throw OverflowException
Console.WriteLine($"Result: {result}");
}
catch (OverflowException ex)
{
Console.WriteLine($"Overflow exception caught: {ex.Message}");
}
- C# 定义的是布尔类型是
bool
,而在运行时里它就是System.Boolean
,在其他 C 语言家族里允许数字类型代表布尔值,比如 0 表示 false ,其他表示 true ,但是在 C# 里不允许这样做,数字不能被自动转换为布尔值。 - 字符用
'c'
限定,字符串用“s”
限定。在 .NET 里的字符串是不可变的,像 ToUpper() 方法会生成一个新的字符串。这样的副作用就是大量字符串会吃内存,增加自动垃圾回收运算量。我们有时候用 StringBuilder 类型来处理字符串,避免内存和CPU占用问题。 - 我们经常会用到 Split() ,Substring() ,Trim() 等字符串方法。对字符串操作,先不要自己写方法,去查查手册或互联网,现成的很多方法写得都很好,不要重复劳动。
- 原始字符串,在表示 JSON 数据时,特别在需要模拟数据时会用到这种语法,让代码很清晰:
static string PersonInfoJson(int age, decimal height)
{
return $$"""
{
"Age": {{age}},
"Height": {{height}},
}
""";
}
- UTF-8 字符串(从 C# 11 开始引入),允许字符串存储为 UTF-8 编码。提高使用大量这种编码的 Web 和网络应用的性能,不只因为 UTF-8 比 UTF-16(.NET 缺省编码)内存效率高,还由于很多 APIs 的结果都是 UTF-8 编码的。举个栗子:
ReadOnlySpan utf8String = "你好世界!"u8; // "Hello, World!" in Chinese
// Convert the UTF-8 span back to a string for display
string decodedString = Encoding.UTF8.GetString(utf8String);
Console.WriteLine(decodedString);
- 元组(Tuple)可以让我们把多个值捆绑在单个值里,使用元组经常要析构它们:
(int X, int Y) point1 = (3, 4);
(int X, int Y) point2 = (12, 16);
(int x, int y) = point1;
Console.WriteLine($"1: {x}, {y}");
(x, y) = point2;
Console.WriteLine($"2: {x}, {y}");
可以用下划线在析构元组时丢弃对应项 (_, int y) = point1;
。
- 动态类型 dynamic 是有 C# 定义的,允许我们忽略编译时的类型检查,最终在运行时 runtime 被解析。在处理只有当运行的时候才能知道数据类型的 COM 对象,反射,和动态语言(比如 Python)时非常有用。
- 对象 Object,对!你没听错,因为 Object 是几乎所有 C# 类型的基类。Object 是终极的容器,你可以用它指向 refer 几乎所有包含对象的变量。
操作符
操作符有常用的数学运算操作符,二进制操作符,布尔操作符和关系操作符。我们还会接触到以下操作符:
int max = (x > y) ? x : y; // 条件操作符
string str1 = s ?? ""; //空值合并操作符
int charCount = s?.Length ?? 0; //空值条件和空值合并操作符
.
程序流
程序流包括:
- 条件判断
if ... else ...
- 多重选择
switch() { case ... default ... }
- 循环
while() ... ; do {...} while ()
- C 风格循环
for () { .. }
- 集合遍历
foreach() { .. }
模式 Patterns
C# 有一个重要的机制: 模式 patterns。模式描述一个或多个标准,根据这些标准对某个值进行测试。
模式经常用在 switch 表达式,if 语句或者方法当中。
- 常数模式
using System;
class ConstantPatternExample
{
static void CheckValue(int number)
{
if (number is 0)
Console.WriteLine("Zero");
else if (number is 1)
Console.WriteLine("One");
else
Console.WriteLine("Another value");
}
static void Main() => CheckValue(1); // Output: One
}
- 类型模式,检查变量类型
using System;
class TypePatternExample
{
static void DisplayObject(object obj)
{
if (obj is string text)
Console.WriteLine($"It's a string with value: {text}");
else if (obj is int number)
Console.WriteLine($"It's an integer with value: {number}");
else
Console.WriteLine("Unknown type");
}
static void Main() => DisplayObject("Hello"); // Output: It's a string with value: Hello
}
- 关系模式,基于变量值的比较
using System;
class RelationalPatternExample
{
static void Evaluate(int score)
{
string result = score switch
{
< 50 => "Fail",
>= 50 and < 75 => "Pass",
>= 75 => "Distinction",
};
Console.WriteLine(result);
}
static void Main() => Evaluate(80); // Output: Distinction
}
还有其他诸如属性模式检查属性值,析构元组中的位置模式等等;有时候内建的模式类型不能提供精确的检测,我们用 when
来添加更多限定:
...
switch
...
case (int position, int distance) when position > distance / 2:
Console.WriteLine("More than half been finished");
break;
...
发表回复