让我们先磨一磨剑,一起回顾必须了解的用 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;
...

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注