C# 入门

时间:2024-03-12 15:17:13

教程:

.NET | 构建。测试。部署。 (microsoft.com)

C# 文档 - 入门、教程、参考。 | Microsoft Learn

C# 数据类型 | (runoob.com)

IDE:

Visual Studio: 面向软件开发人员和 Teams 的 IDE 和代码编辑器 (microsoft.com)

Rider:JetBrains 出品的跨平台 .NET IDE

最新版本:

.NET8下载

下载 .NET(Linux、macOS 和 Windows) (microsoft.com)

.NET 8 的新增功能

.NET 8 的新增功能 | Microsoft Learn

C# 应用领域

  • 人工智能
  • 物联网
  • 桌面开发
  • 网页开发
  • 游戏开发
  • 云应用
  • 移动应用

目前唯一一种能同时涵盖这些领域的语言

C# 误区

C#开源跨平台

CLI 和 ClR

CLI:Common LanguageInfrastructure 公共语言基础框架

  • 用来处理代码编译过程
  • 类似Java代码编译为字节码的过程

CLR:Common LanguageRuntime公共语言运行时(服务环境)

  • 代码运行环境
  • 相当于Java的JVM虚拟机

CLI + CLR = .NET Core

Hello World

1、Visual Studio创建控制台应用 MyApp

从 .NET 6 开始,新 C# 控制台应用的项目模板在 Program.cs 文件中生成以下代码:

// See https://aka.ms/new-console-template for more information
Console.WriteLine("Hello, World!");

新的输出使用最新的 C# 功能,这些功能简化了需要为程序编写的代码。 对于 .NET 5 及更早版本,控制台应用模板生成以下代码:

using System;
namespace MyApp // Note: actual namespace depends on the project name.
{
    internal class Program
    {
        static void Main(string[] args)
        {
            /* 我的第一个 C# 程序*/
            Console.WriteLine("Hello World!");
            Console.ReadKey();
        }
    }
}

让我们看一下上面程序的各个部分:

  • 程序的第一行 using System; - using 关键字用于在程序中包含 System 命名空间。 一个程序一般有多个 using 语句。

  • 下一行是 namespace 声明。一个 namespace 里包含了一系列的类。MyApp 命名空间包含了类 Program

  • 下一行是 class 声明。类 Program 包含了程序使用的数据和方法声明。类一般包含多个方法。方法定义了类的行为。在这里,Program 类只有一个 Main 方法。

  • 下一行定义了 Main 方法,是所有 C# 程序的 入口点Main 方法说明当执行时 类将做什么动作。

  • 下一行 // 将会被编译器忽略,且它会在程序中添加额外的 注释

  • Main 方法通过语句

    Console.WriteLine(“Hello World”);

    指定了它的行为。

    WriteLine 是一个定义在 System 命名空间中的 Console 类的一个方法。该语句会在屏幕上显示消息 “Hello World”。

  • 最后一行 Console.ReadKey(); 是针对 VS.NET 用户的。这使得程序会等待一个按键的动作,防止程序从 Visual Studio .NET 启动时屏幕会快速运行并关闭。

以下几点值得注意:

  • C# 是大小写敏感的。
  • 所有的语句和表达式必须以分号(;)结尾。
  • 程序的执行从 Main 方法开始。
  • 与 Java 不同的是,文件名可以不同于类的名称。

控制台 System.Console

介绍

System.Console 类 - .NET | Microsoft Learn

方法

Console 类 (System) | Microsoft Learn

namespace ConsoleApp1 // Note: actual namespace depends on the project name.
{
    public static class Program
    {
        public static void Main(string[] args)
        {
            Console.WriteLine("Hello World");
            Console.WriteLine("Hello World");
          
            Console.WriteLine("Welcome");
            Console.WriteLine(" To");
            Console.WriteLine(" C#");
          
            ConsoleKeyInfo a = Console.ReadKey();
          
            Console.WriteLine(a.Key);
            Console.Clear();
            Console.Read();
        }
    }
}

C# 基本语法

using System;
namespace RectangleApplication
{
    class Rectangle
    {
        // 成员变量
        double length;
        double width;
        public void Acceptdetails()
        {
            length = 4.5;    
            width = 3.5;
        }
        public double GetArea()
        {
            return length * width;
        }
        public void Display()
        {
            Console.WriteLine("Length: {0}", length);
            Console.WriteLine("Width: {0}", width);
            Console.WriteLine("Area: {0}", GetArea());
        }
    }
    
    class ExecuteRectangle
    {
        static void Main(string[] args)
        {
            Rectangle r = new Rectangle();
            r.Acceptdetails();
            r.Display();
            Console.ReadLine();
        }
    }
}

using 关键字

在任何 C# 程序中的第一条语句都是:

using System;

using 关键字用于在程序中包含命名空间。一个程序可以包含多个 using 语句。

class 关键字

class 关键字用于声明一个类。

C# 中的注释

注释是用于解释代码。编译器会忽略注释的条目。在 C# 程序中,多行注释以 /* 开始,并以字符 */ 终止,如下所示:

/* 这个程序演示
C# 的注释
使用 */

单行注释是用 // 符号表示。例如:

// 这一行是注释 

成员变量

变量是类的属性或数据成员,用于存储数据。在上面的程序中,Rectangle 类有两个成员变量,名为 lengthwidth

成员函数

函数是一系列执行指定任务的语句。类的成员函数是在类内声明的。我们举例的类 Rectangle 包含了三个成员函数: AcceptDetailsGetAreaDisplay

实例化一个类

在上面的程序中,类 ExecuteRectangle 是一个包含 Main() 方法和实例化 Rectangle 类的类。

标识符

标识符是用来识别类、变量、函数或任何其它用户定义的项目。在 C# 中,类的命名必须遵循如下基本规则:

  • 标识符必须以字母、下划线或 @ 开头,后面可以跟一系列的字母、数字( 0 - 9 )、下划线( _ )、@。
  • 标识符中的第一个字符不能是数字。
  • 标识符必须不包含任何嵌入的空格或符号,比如 ? - +! # % ^ & * ( ) [ ] { } . ; : " ’ / \。
  • 标识符不能是 C# 关键字。除非它们有一个 @ 前缀。 例如,@if 是有效的标识符,但 if 不是,因为 if 是关键字。
  • 标识符必须区分大小写。大写字母和小写字母被认为是不同的字母。
  • 不能与C#的类库名称相同。

C# 关键字

关键字是 C# 编译器预定义的保留字。这些关键字不能用作标识符,但是,如果您想使用这些关键字作为标识符,可以在关键字前面加上 @ 字符作为前缀。

在 C# 中,有些关键字在代码的上下文中有特殊的意义,如 get 和 set,这些被称为上下文关键字(contextual keywords)。

下表列出了 C# 中的保留关键字(Reserved Keywords)和上下文关键字(Contextual Keywords):

保留关键字
abstract as base bool break byte case
catch char checked class const continue decimal
default delegate do double else enum event
explicit extern false finally fixed float for
foreach goto if implicit in in (generic modifier) int
interface internal is lock long namespace new
null object operator out out (generic modifier) override params
private protected public readonly ref return sbyte
sealed short sizeof stackalloc static string struct
switch this throw true try typeof uint
ulong unchecked unsafe ushort using virtual void
volatile while
上下文关键字
add alias ascending descending dynamic from get
global group into join let orderby partial (type)
partial (method) remove select set

变量(Variable)与数据类型(Data Type)

在 C# 中,变量分为以下几种类型:

  • 值类型(Value types)
  • 引用类型(Reference types)
  • 指针类型(Pointer types)

值类型(Value types)(基本类型)

值类型变量可以直接分配给一个值。它们是从类 System.ValueType 中派生的。

值类型直接包含数据。比如 int、char、float,它们分别存储数字、字符、浮点数。当您声明一个 int 类型时,系统分配内存来存储值。

下表列出了 C# 2010 中可用的值类型:

类型 描述 范围 默认值
bool 布尔值 True 或 False False
byte 8 位无符号整数 0 到 255 0
char 16 位 Unicode 字符 U +0000 到 U +ffff ‘\0’
decimal 128 位精确的十进制值,28-29 有效位数 (-7.9 x 1028 到 7.9 x 1028) / 100 到 28 0.0M
double 64 位双精度浮点型 (+/-)5.0 x 10-324 到 (+/-)1.7 x 10308 0.0D
float 32 位单精度浮点型 -3.4 x 1038 到 + 3.4 x 1038 0.0F
int 32 位有符号整数类型 -2,147,483,648 到 2,147,483,647 0
long 64 位有符号整数类型 -9,223,372,036,854,775,808 到 9,223,372,036,854,775,807 0L
sbyte 8 位有符号整数类型 -128 到 127 0
short 16 位有符号整数类型 -32,768 到 32,767 0
uint 32 位无符号整数类型 0 到 4,294,967,295 0
ulong 64 位无符号整数类型 0 到 18,446,744,073,709,551,615 0
ushort 16 位无符号整数类型 0 到 65,535 0
  • u = unsigned,无符号的意思

  • s = signed,有符号的意思

  • char无法表示中文:中文标准是GBK、不属于UTF-16

  • 内建类型(引用类型),但不属于基本类型

    • 中文得用string类型
      • string可以通过把多个Unicode字符拼接起来
      • 显示中文、甚至是emoji
    • object对象类型
    • dynamic动态类型

如需得到一个类型或一个变量在特定平台上的准确尺寸,可以使用 sizeof 方法。表达式 sizeof(type) 产生以字节为单位存储对象或类型的存储尺寸。下面举例获取任何机器上 int 类型的存储尺寸:

实例
using System;

namespace DataTypeApplication
{
   class Program
   {
      static void Main(string[] args)
      {
         Console.WriteLine("Size of int: {0}", sizeof(int));
         Console.ReadLine();
      }
   }
}

当上面的代码被编译和执行时,它会产生下列结果:

Size of int: 4

引用类型(Reference types)

引用类型不包含存储在变量中的实际数据,但它们包含对变量的引用。

换句话说,它们指的是一个内存位置。使用多个变量时,引用类型可以指向一个内存位置。如果内存位置的数据是由一个变量改变的,其他变量会自动反映这种值的变化。内置的 引用类型有:objectdynamicstring

对象(Object)类型

对象(Object)类型 是 C# 通用类型系统(Common Type System - CTS)中所有数据类型的终极基类。Object 是 System.Object 类的别名。所以对象(Object)类型可以被分配任何其他类型(值类型、引用类型、预定义类型或用户自定义类型)的值。但是,在分配值之前,需要先进行类型转换。

当一个值类型转换为对象类型时,则被称为 装箱;另一方面,当一个对象类型转换为值类型时,则被称为 拆箱

object obj;
obj = 100; // 这是装箱
动态(Dynamic)类型
背景知识
  • 语言分类:静态类型语言和动态类型语言
  • 静态类型语言会在编译的时候检查数据类型 C++、Java、C#
  • 而动态类型则相反,直到程序运行起来,数据类型才会被确定下来 JavaScript、python
强类型 VS 弱类型
  • 从类型能否转换的角度来说
  • var answer = 22 * "3";
  • C# 不合法(强语言,类型安全)JavaScript 合法(弱语言,类型不安全)
C# 属于强语言,同时略偏静态类型
  • 强类型语言,因为它不支持类型的自动转化
  • 略偏静态类型 ??
  • 主要是静态语言,但是因为存在dynamic这个关键词,所以它也具有动态类型的特点

您可以存储任何类型的值在动态数据类型变量中。这些变量的类型检查是在运行时发生的。

声明动态类型的语法:

dynamic <variable_name> = value;

例如:

dynamic d = 20;

动态类型与对象类型相似,但是对象类型变量的类型检查是在编译时发生的,而动态类型变量的类型检查是在运行时发生的。

字符串(String)类型

字符串(String)类型 允许您给变量分配任何字符串值。字符串(String)类型是 System.String 类的别名。它是从对象(Object)类型派生的。字符串(String)类型的值可以通过两种形式进行分配:引号和 @引号。

例如:

String str = "runoob.com";

一个 @引号字符串:

@"runoob.com";

C# string 字符串的前面可以加 @(称作"逐字字符串")将转义字符(\)当作普通字符对待,比如:

string str = @"C:\Windows";

等价于:

string str = "C:\\Windows";

@ 字符串中可以任意换行,换行符及缩进空格都计算在字符串长度之内。

string str = @"<script type=""text/javascript"">
    <!--
    -->
</script>";

用户自定义引用类型有:class、interface 或 delegate。我们将在以后的章节中讨论这些类型。

指针类型(Pointer types)

指针类型变量存储另一种类型的内存地址。C# 中的指针与 C 或 C++ 中的指针有相同的功能。

声明指针类型的语法:

type* identifier;

例如:

char* cptr;
int* iptr;

我们将在章节"不安全的代码"中讨论指针类型。

字符串方法与操作

内置引用类型 - C# 引用 - C# | Microsoft Learn

string 类型表示零个或多个 Unicode 字符的序列。 stringSystem.String 在 .NET 中的别名。

尽管 string 为引用类型,但是定义相等运算符 == 和 != 是为了比较 string 对象(而不是引用)的值。 基于值的相等性使得对字符串相等性的测试更为直观。 例如:

string a = "hello";
string b = "h";
// Append to contents of 'b'
b += "ello";
Console.WriteLine(a == b);
Console.WriteLine(object.ReferenceEquals(a, b));

前面的示例显示“True”,然后显示“False”,因为字符串的内容是相等的,但 ab 并不指代同一字符串实例。

+ 运算符连接字符串:

string a = "good " + "morning";

前面的代码会创建一个包含“good morning”的字符串对象。

字符串是不可变的,即:字符串对象在创建后,其内容不可更改。 例如,编写此代码时,编译器实际上会创建一个新的字符串对象来保存新的字符序列,且该新对象将赋给 b。 已为 b 分配的内存(当它包含字符串“h”时)可用于垃圾回收。

string b = "h";
b += "ello";

[]运算符可用于只读访问字符串的个别字符。 有效索引于 0 开始,且必须小于字符串的长度:

string str = "test";
char x = str[2];  // x = 's';

同样,[] 运算符也可用于循环访问字符串中的每个字符:

string str = "test";

for (int i = 0; i < str.Length; i++)
{
  Console.Write(str[i] + " ");
}
// Output: t e s t

字符串文本

字符串字面量属于 string 类型且可以三种形式编写(原始、带引号和逐字)。

原始字符串字面量从 C# 11 开始可用。 字符串字面量可以包含任意文本,而无需转义序列。 字符串字面量可以包括空格和新行、嵌入引号以及其他特殊字符。 原始字符串字面量用至少三个双引号 (“”") 括起来:

"""
This is a multi-line
    string literal with the second line indented.
"""

甚至可以包含三个(或更多)双引号字符序列。 如果文本需要嵌入的引号序列,请根据需要使用更多引号开始和结束原始字符串字面量:

"""""
This raw string literal has four """", count them: """" four!
embedded quote characters in a sequence. That's why it starts and ends
with five double quotes.

You could extend this example with as many embedded quotes as needed for your text.
"""""

原始字符串字面量的起始和结束引号序列通常位于与嵌入文本不同的行上。 多行原始字符串字面量支持自带引号的字符串:

var message = """
"This is a very important message."
""";
Console.WriteLine(message);
// output: "This is a very important message."

当起始引号和结束引号在不同的行上时,则最终内容中不包括起始引号之后和结束引号之前的换行符。 右引号序列指示字符串字面量的最左侧列。 可以缩进原始字符串字面量以匹配整体代码格式:

var message = """
    "This is a very important message."
    """;
Console.WriteLine(message);
// output: "This is a very important message."
// The leftmost whitespace is not part of the raw string literal

保留结束引号序列右侧的列。 此行为将为 JSON、YAML 或 XML 等数据格式启用原始字符串,如以下示例所示:

var json= """
    {
        "prop": 0
    }
    """;

如果任何文本行扩展到右引号序列的左侧,编译器将发出错误。 左引号和右引号序列可以位于同一行上,前提是字符串字面量既不能以引号字符开头,也不能以引号字符结尾:

var shortText = """He said "hello!" this morning.""";

可以将原始字符串字面量与字符串内插相结合,以在输出字符串中包含引号字符和大括号。

带引号字符串括在双引号 (") 内。

"good morning"  // a string literal

字符串文本可包含任何字符文本。 包括转义序列。 下面的示例使用转义序列 \\ 表示反斜杠,使用 \u0066 表示字母 f,以及使用 \n 表示换行符。

string a = "\\\u0066\n F";
Console.WriteLine(a);
// Output:
// \f
//  F

转义码 \udddd(其中 dddd 是一个四位数字)表示 Unicode 字符 U+dddd。 另外,还可识别八位 Unicode 转义码:\Udddddddd

逐字字符串文本@ 开头,并且也括在双引号内。 例如:

@"good morning"  // a string literal

逐字字符串的优点是不处理转义序列,这样就可轻松编写。 例如,以下文本与完全限定的 Windows 文件名匹配:

@"c:\Docs\Source\a.txt"  // rather than "c:\\Docs\\Source\\a.txt"

若要在用 @ 引起来的字符串中包含双引号,双倍添加即可:

@"""Ahoy!"" cried the captain." // "Ahoy!" cried the captain.

UTF-8 字符串字面量

.NET 中的字符串是使用 UTF-16 编码存储的。 UTF-8 是 Web 协议和其他重要库的标准。 从 C# 11 开始,可以将 u8 后缀添加到字符串字面量以指定 UTF-8 编码。 UTF-8 字面量存储为 ReadOnlySpan<byte> 对象。 UTF-8 字符串字面量的自然类型是 ReadOnlySpan<byte>。 使用 UTF-8 字符串字面量创建的声明比声明等效的 System.ReadOnlySpan 更清晰,如以下代码所示:

ReadOnlySpan<byte> AuthWithTrailingSpace = new byte[] { 0x41, 0x55, 0x54, 0x48, 0x20 };
ReadOnlySpan<byte> AuthStringLiteral = "AUTH "u8;

要将 UTF-8 字符串字面量存储为数组,需使用 ReadOnlySpan.ToArray() 将包含字面量的字节复制到可变数组:

byte[] AuthStringLiteral = "AUTH "u8.ToArray();

UTF-8 字符串字面量不是编译时常量;而是运行时常量。 因此,不能将其用作可选参数的默认值。 UTF-8 字符串字面量不能与字符串内插结合使用。 不能对同一字符串表达式使用 $ 令牌和 u8 后缀。

使用 $ 的字符串内插

$ - 字符串内插 - 格式字符串输出 - C# | Microsoft Learn

$ 字符将字符串字面量标识为内插字符串。 内插字符串是可能包含内插表达式的字符串文本 。 将内插字符串解析为结果字符串时,带有内插表达式的项会替换为表达式结果的字符串表示形式。

字符串内插为格式化字符串提供了一种可读性和便捷性更高的方式。 它比字符串复合格式设置更容易阅读。 下面的示例使用了这两种功能生成同样的输出结果:

var name = "Mark";
var date = DateTime.Now;

// Composite formatting:
Console.WriteLine("Hello, {0}! Today is {1}, it's {2:HH:mm} now.", name, date.DayOfWeek, date);
// String interpolation:
Console.WriteLine($"Hello, {name}! Today is {date.DayOfWeek}, it's {date:HH:mm} now.");
// Both calls produce the same output that is similar to:
// Hello, Mark! Today is Wednesday, it's 19:40 now.

从 C# 10 开始,可以使用内插字符串来初始化常量字符串。 仅当内插字符串中的所有内插表达式也是常量字符串时,才可以执行此操作。

内插字符串的结构

若要将字符串标识为内插字符串,可在该字符串前面加上 $ 符号。 字符串字面量开头的 $" 之间不能有任何空格。

具备内插表达式的项的结构如下所示:

{<interpolationExpression>[,<alignment>][:<formatString>]}

括号中的元素是可选的。 下表说明了每个元素:

元素 描述
interpolationExpression 生成需要设置格式的结果的表达式。null 的字符串表示形式为 String.Empty
alignment 常数表达式,它的值定义表达式结果的字符串表示形式中的最小字符数。 如果值为正,则字符串表示形式为右对齐;如果值为负,则为左对齐。 有关详细信息,请参阅复合格式设置一文的对齐组件部分。
formatString 受表达式结果类型支持的格式字符串。 有关详细信息,请参阅复合格式设置一文的格式字符串组件部分。

以下示例使用上述可选的格式设置组件:

Console.WriteLine($"|{"Left",-7}|{"Right",7}|");

const int FieldWidthRightAligned = 20;
Console.WriteLine($"{Math.PI,FieldWidthRightAligned} - default formatting of the pi number");
Console.WriteLine($"{Math.PI,FieldWidthRightAligned:F3} - display only three decimal digits of the pi number");
// Output is:
// |Left   |  Right|
//     3.14159265358979 - default formatting of the pi number
//                3.142 - display only three decimal digits of the pi number

从 C# 11 开始,可以在内插表达式中使用换行符,以使表达式的代码更具可读性。 下面的示例展示了换行符如何提高涉及模式匹配的表达式的可读性:

string message = $"The usage policy for {safetyScore} is {
    safetyScore switch
    {
        > 90 => "Unlimited usage",
        > 80 => "General usage, with daily safety check",
        > 70 => "Issues must be addressed within 1 week",
        > 50 => "Issues must be addressed within 1 day",
        _ => "Issues must be addressed before continued use",
    }
    }";
内插原始字符串字面量

从 C# 11 开始,可以使用内插原始字符串字面量,如以下示例所示:

int X = 2;
int Y = 3;

var pointMessage = $"""The point "{X}, {Y}" is {Math.Sqrt(X * X + Y * Y):F3} from the origin""";

Console.WriteLine(pointMessage);
// Output is:
// The point "2, 3" is 3.606 from the origin

要在结果字符串中嵌入 {} 字符,请让内插原始字符串字面量以多个 $ 字符开头。 执行此操作时,任何长度短于 $ 字符数的 {} 字符序列都会嵌入到结果字符串中。 若要将任何内插表达式包含在该字符串中,需要使用与 $ 字符数相同的大括号数,如以下示例所示:

int X = 2;
int Y = 3;

var pointMessage = $$"""{The point {{{X}}, {{Y}}} is {{Math.Sqrt(X * X + Y * Y):F3}} from the origin}""";
Console.WriteLine(pointMessage);
// Output is:
// {The point {2, 3} is 3.606 from the origin}

在前面的示例中,内插原始字符串字面量以两个 $ 字符开头。 这就是为什么你需要将每个内插表达式放在双大括号 {{}} 之间。 单个大括号嵌入到结果字符串中。 如果需要将重复的 {} 字符嵌入结果字符串中,请使用相应增加的 $ 字符数来指定内插原始字符串字面量。

特殊字符

要在内插字符串生成的文本中包含大括号 “{” 或 “}”,请使用两个大括号,即 “{{” 或 “}}”。 有关详细信息,请参阅复合格式设置一文的转义括号部分。

因为冒号(“:”)在内插表达式项中具有特殊含义,为了在内插表达式中使用条件运算符,请将表达式放在括号内。

以下示例演示了如何在结果字符串中包括大括号。 它还演示了如何使用条件运算符:

string name = "Horace";
int age = 34;
Console.WriteLine($"He asked, \"Is your name {name}?\", but didn't wait for a reply :-{{");
Console.WriteLine($"{name} is {age} year{(age == 1 ? "" : "s")} old.");
// Output is:
// He asked, "Is your name Horace?", but didn't wait for a reply :-{
// Horace is 34 years old.

内插逐字字符串$@ 字符开头。 可以按任意顺序使用 $@$@"..."@$"..." 均为有效的内插逐字字符串。 有关逐字字符串的详细信息,请参阅字符串逐字标识符文章。

特定于区域性的格式设置

默认情况下,内插字符串将 CultureInfo.CurrentCulture 属性定义的当前区域性用于所有格式设置操作。

要将内插字符串解析为特定于区域性的结果字符串,请使用 String.Create(IFormatProvider, DefaultInterpolatedStringHandler) 方法,该方法从 .NET 6 开始可用。 下面的示例演示如何执行此操作:

double speedOfLight = 299792.458;

System.Globalization.CultureInfo.CurrentCulture = System.Globalization.CultureInfo.GetCultureInfo("nl-NL");
string messageInCurrentCulture = $"The speed of light is {speedOfLight:N3} km/s.";

var specificCulture = System.Globalization.CultureInfo.GetCultureInfo("en-IN");
string messageInSpecificCulture = string.Create(
    specificCulture, $"The speed of light is {speedOfLight:N3} km/s.");

string messageInInvariantCulture = string.Create(
    System.Globalization.CultureInfo.InvariantCulture, $"The speed of light is {speedOfLight:N3} km/s.");

Console.WriteLine($"{System.Globalization.CultureInfo.CurrentCulture,-10} {messageInCurrentCulture}");
Console.WriteLine($"{specificCulture,-10} {messageInSpecificCulture}");
Console.WriteLine($"{"Invariant",-10} {messageInInvariantCulture}");
// Output is:
// nl-NL      The speed of light is 299.792,458 km/s.
// en-IN      The speed of light is 2,99,792.458 km/s.
// Invariant  The speed of light is 299,792.458 km/s.

在 .NET 5 和更早的 .NET 版本中,请使用从内插字符串到 FormattableString 实例的隐式转换。 然后,可以使用实例 FormattableString.ToString(IFormatProvider) 方法或静态 FormattableString.Invariant 方法来生成特定于区域性的结果字符串。 下面的示例演示如何执行此操作:

double speedOfLight = 299792.458;
FormattableString message = $"The speed of light is {speedOfLight:N3} km/s.";

var specificCulture = System.Globalization.CultureInfo.GetCultureInfo("en-IN");
string messageInSpecificCulture = message.ToString(specificCulture);
Console.WriteLine(messageInSpecificCulture);
// Output:
// The speed of light is 2,99,792.458 km/s.

string messageInInvariantCulture = FormattableString.Invariant(message);
Console.WriteLine(messageInInvariantCulture);
// Output is:
// The speed of light is 299,792.458 km/s.

有关自定义格式设置的详细信息,请参阅在 .NET 中设置类型格式一文中的使用 ICustomFormatter 进行自定义格式设置部分。

其他资源

如果你不熟悉字符串内插,请参阅 C# 中的字符串内插交互式教程。 还可查看另一个 C# 中的字符串内插教程。 该教程演示了如何使用内插字符串生成带格式的字符串。

内插字符串编译

从 C# 10 和 .NET 6 开始,编译器会检查内插字符串是否被分配给满足内插字符串处理程序模式要求的类型。 内插字符串处理程序是一种将内插字符串转换为结果字符串的类型。 当内插字符串的类型为 string 时,它由 System.Runtime.CompilerServices.DefaultInterpolatedStringHandler 处理。 有关自定义内插字符串处理程序的示例,请参阅编写自定义字符串内插处理程序教程。 使用内插字符串处理程序是一种高级方案,通常出于性能原因而需要使用。

内插字符串处理程序的一个副作用是,自定义处理程序(包括 System.Runtime.CompilerServices.DefaultInterpolatedStringHandler)可能不会在所有条件下都计算内插字符串中的所有内插表达式。 这意味着这些表达式的副作用可能不会发生。

在 C# 10 之前,如果内插字符串类型为 string,则通常将其转换为 String.Format 方法调用。 如果分析的行为等同于串联,则编译器可将 String.Format 替换为 String.Concat

如果内插字符串类型为 IFormattableFormattableString,则编译器会生成对 FormattableStringFactory.Create 方法的调用。

逐字文本 - @ 在变量、属性和字符串文本中

@ 特殊字符用作原义标识符。 通过以下方式使用它:

  1. 指示将原义解释字符串。 @ 字符在此实例中定义原义标识符 。 简单转义序列(如代表反斜杠的 "\\")、十六进制转义序列(如代表大写字母 A 的 "\x0041")和 Unicode 转义序列(如代表大写字母 A 的 "\u0041")都将按字面解释。 只有引号转义序列 ("") 不会按字面解释;因为它生成一个双引号。 此外,如果是逐字内插字符串,大括号转义序列({{}})不按字面解释;它们会生成单个大括号字符。 下面的示例分别使用常规字符串和原义字符串定义两个相同的文件路径。 这是原义字符串的较常见用法之一。
string filename1 = @"c:\documents\files\u0066.txt";
string filename2 = "c:\\documents\\files\\u0066.txt";

Console.WriteLine(filename1);
Console.WriteLine(filename2);
// The example displays the following output:
//     c:\documents\files\u0066.txt
//     c:\documents\files\u0066.txt

下面的示例演示定义包含相同字符序列的常规字符串和原义字符串的效果。

string s1 = "He said, \"This is the last \u0063hance\x0021\"";
string s2 = @"He said, ""This is the last \u0063hance\x0021""";

Console.WriteLine(s1);
Console.WriteLine(s2);
// The example displays the following output:
//     He said, "This is the last chance!"
//     He said, "This is the last \u0063hance\x0021"
  1. 使用 C# 关键字作为标识符。 @ 字符可作为代码元素的前缀,编译器将把此代码元素解释为标识符而非 C# 关键字。 下面的示例使用 @ 字符定义其在 for 循环中使用的名为 for 的标识符。
string[] @for = { "John", "James", "Joan", "Jamie" };
for (int ctr = 0; ctr < @for.Length; ctr++)
{
   Console.WriteLine($"Here is your gift, {@for[ctr]}!");
}
// The example displays the following output:
//     Here is your gift, John!
//     Here is your gift, James!
//     Here is your gift, Joan!
//     Here is your gift, Jamie!
  1. 使编译器在命名冲突的情况下区分两种属性。 属性是派生自 Attribute 的类。 其类型名称通常包含后缀 Attribute,但编译器不会强制进行此转换。 随后可在代码中按其完整类型名称(例如 [InfoAttribute])或短名称(例如 [Info])引用此属性。 但是,如果两个短名称相同,并且一个类型名称包含 Attribute 后缀而另一类型名称不包含,则会出现命名冲突。 例如,由于编译器无法确定将 Info 还是 InfoAttribute 属性应用于 Example 类,因此下面的代码无法编译。 有关详细信息,请参阅 CS1614
using System;

[AttributeUsage(AttributeTargets.Class)]
public class Info : Attribute
{
   private string information;

   public Info(string info)
   {
      information = info;
   }
}

[AttributeUsage(AttributeTargets.Method)]
public class InfoAttribute : Attribute
{
   private string information;

   public InfoAttribute(string info)
   {
      information = info;
   }
}

[Info("A simple executable.")] // Generates compiler error CS1614. Ambiguous Info and InfoAttribute.
// Prepend '@' to select 'Info' ([@Info("A simple executable.")]). Specify the full name 'InfoAttribute' to select it.
public class Example
{
   [InfoAttribute("The entry point.")]
   public static void Main()
   {
   }
}

决策与分支

选择语句 - ifif-elseswitch

ifif-elseswitch 语句根据表达式的值从多个可能的语句选择要执行的路径。 仅当提供的布尔表达式的计算结果为 true 时,ifif 语句才执行语句。 语句 if-else允许你根据布尔表达式选择要遵循的两个代码路径中的哪一个。 switch 语句根据与表达式匹配的模式来选择要执行的语句列表。

if 语句

if 语句可采用以下两种形式中的任一种:

  • 包含 else 部分的 if 语句根据布尔表达式的值选择两个语句中的一个来执行,如以下示例所示:
DisplayWeatherReport(15.0);  // Output: Cold.
DisplayWeatherReport(24.0);  // Output: Perfect!

void DisplayWeatherReport(double tempInCelsius)
{
    if (tempInCelsius < 20.0)
    {
        Console.WriteLine("Cold.");
    }
    else
    {
        Console.WriteLine("Perfect!");
    }
}
  • 不包含 else 部分的 if 语句仅在布尔表达式计算结果为 true 时执行其主体,如以下示例所示:
DisplayMeasurement(45);  // Output: The measurement value is 45
DisplayMeasurement(-3);  // Output: Warning: not acceptable value! The measurement value is -3

void DisplayMeasurement(double value)
{
    if (value < 0 || value > 100)
    {
        Console.Write("Warning: not acceptable value! ");
    }

    Console.WriteLine($"The measurement value is {value}");
}

可嵌套 if 语句来检查多个条件,如以下示例所示:

DisplayCharacter('f');  // Output: A lowercase letter: f
DisplayCharacter('R');  // Output: An uppercase letter: R
DisplayCharacter('8');  // Output: A digit: 8
DisplayCharacter(',');  // Output: Not alphanumeric character: ,

void DisplayCharacter(char ch)
{
    if (char.IsUpper(ch))
    {
        Console.WriteLine($"An uppercase letter: {ch}");
    }
    else if (char.IsLower(ch))
    {
        Console.WriteLine($"A lowercase letter: {ch}");
    }
    else if (char.IsDigit(ch))
    {
        Console.WriteLine($"A digit: {ch}");
    }
    else
    {
        Console.WriteLine($"Not alphanumeric character: {ch}");
    }
}

在表达式上下文中,可使用条件运算符 ?: 根据布尔表达式的值计算两个表达式中的一个。

switch 语句

switch 语句根据与匹配表达式匹配的模式来选择要执行的语句列表,如以下示例所示:

DisplayMeasurement(-4);  // Output: Measured value is -4; too low.
DisplayMeasurement(5);  // Output: Measured value is 5.
DisplayMeasurement(30);  // Output: Measured value is 30; too high.
DisplayMeasurement(double.NaN);  // Output: Failed measurement.

void DisplayMeasurement(double measurement)
{
    switch (measurement)
    {
        case < 0.0:
            Console.WriteLine($"Measured value is {measurement}; too low.");
            break;

        case > 15.0:
            Console.WriteLine($"Measured value is {measurement}; too high.");
            break;

        case double.NaN:
            Console.WriteLine("Failed measurement.");
            break;

        default:
            Console.WriteLine($"Measured value is {measurement}.");
            break;
    }
}

在上述示例中,switch 语句使用以下模式:

  • 关系模式:用于将表达式结果与常量进行比较。
  • 常量模式:测试表达式结果是否等于常量。

重要

有关 switch 语句支持的模式的信息,请参阅模式

上述示例还展示了 default case。 default case 指定匹配表达式与其他任何 case 模式都不匹配时要执行的语句。 如果匹配表达式与任何 case 模式都不匹配,且没有 default case,控制就会贯穿 switch 语句。

switch 语句执行第一个 switch 部分中的语句列表,其 case 模式与匹配表达式匹配,并且它的 case guard(如果存在)求值为 trueswitch 语句按文本顺序从上到下对 case 模式求值。 编译器在 switch 语句包含无法访问的 case 时会生成错误。 这种 case 已由大写字母处理或其模式无法匹配。

备注

default case 可以在 switch 语句的任何位置出现。 无论其位置如何,仅当所有其他事例模式都不匹配或 goto default; 语句在其中一个 switch 节中执行时,default 才会计算事例。

可以为 switch 语句的一部分指定多个 case 模式,如以下示例所示:

DisplayMeasurement(-4);  // Output: Measured value is -4; out of an acceptable range.
DisplayMeasurement(50);  // Output: Measured value is 50.
DisplayMeasurement(132);  // Output: Measured value is 132; out of an acceptable range.

void DisplayMeasurement(int measurement)
{
    switch (measurement)
    {
        case < 0:
        case > 100:
            Console.WriteLine($"Measured value is {measurement}; out of an acceptable range.");
            break;
      
        default:
            Console.WriteLine($"Measured value is {measurement}.");
            break;
    }
}

switch 语句中,控制不能从一个 switch 部分贯穿到下一个 switch 部分。 如本部分中的示例所示,通常使用每个 switch 部分末尾的 break 语句将控制从 switch 语句传递出去。 还可使用 returnthrow 语句将控制从 switch 语句传递出去。 若要模拟贯穿行为,将控制传递给其他 switch 部分,可使用 goto 语句

在表达式上下文中,可使用 switch 表达式,根据与表达式匹配的模式,对候选表达式列表中的单个表达式进行求值。

Case guard

case 模式可能表达功能不够,无法指定用于执行 switch 部分的条件。 在这种情况下,可以使用 case guard。 这是一个附加条件,必须与匹配模式同时满足。 case guard 必须是布尔表达式。 可以在模式后面的 when 关键字之后指定一个 case guard,如以下示例所示:

DisplayMeasurements(3, 4);  // Output: First measurement is 3, second measurement is 4.
DisplayMeasurements(5, 5);  // Output: Both measurements are valid and equal to 5.

void DisplayMeasurements(int a, int b)
{
    switch ((a, b))
    {
        case (> 0, > 0) when a == b:
            Console.WriteLine($"Both measurements are valid and equal to {a}.");
            break;

        case (> 0, > 0):
            Console.WriteLine($"First measurement is {a}, second measurement is {b}.");
            break;

        default:
            Console.WriteLine("One or both measurements are not valid.");
            break;
    }
}

上述示例使用带有嵌套关系模式位置模式

?: 运算符 - 三元条件运算符

条件运算符 (?:) 也称为三元条件运算符,用于计算布尔表达式,并根据布尔表达式的计算结果为 true 还是 false 来返回两个表达式中的一个结果,如以下示例所示:

string GetWeatherDisplay(double tempInCelsius) => tempInCelsius < 20.0 ? "Cold." : "Perfect!";

Console.WriteLine(GetWeatherDisplay(15));  // output: Cold.
Console.WriteLine(GetWeatherDisplay(27));  // output: Perfect!

如上述示例所示,条件运算符的语法如下所示:

condition ? consequent : alternative

condition 表达式的计算结果必须为 truefalse。 若 condition 的计算结果为 true,将计算 consequent,其结果成为运算结果。 若 condition 的计算结果为 false,将计算 alternative,其结果成为运算结果。 只会计算 consequentalternative。 条件表达式是目标类型的。 也就是说,如果条件表达式的目标类型是已知的,则 consequentalternative 的类型必须可隐式转换为目标类型,如以下示例所示:

var rand = new Random();
var condition = rand.NextDouble() > 0.5;

int? x = condition ? 12 : null;

IEnumerable<int> xs = x is null ? new List<int>() { 0, 1 } : new int[] { 2, 3 };

如果条件表达式的目标类型未知(例如使用 var 关键字时)或 consequentalternative 的类型必须相同,或者必须存在从一种类型到另一种类型的隐式转换:

var rand = new Random();
var condition = rand.NextDouble() > 0.5;

var x = condition ? 12 : (int?)null;

条件运算符为右联运算符,即形式的表达式

a ? b : c ? d : e

计算结果为

a ? b : (c ? d : e)
  • 提示

可以使用以下助记键设备记住条件运算符的计算方式:

is this condition true ? yes : no
ref 条件表达式

条件 ref 表达式可有条件地返回变量引用,如以下示例所示:

int[] smallArray = [1, 2, 3, 4, 5];
int[] largeArray = [10, 20, 30, 40, 50];

int index = 7;
ref int refValue = ref ((index < 5) ? ref smallArray[index] : ref largeArray[index - 5]);
refValue = 0;

index = 2;
((index < 5) ? ref smallArray[index] : ref largeArray[index - 5]) = 100;

Console.WriteLine(string.Join(" ", smallArray));
Console.WriteLine(string.Join(" ", largeArray));
// Output:
// 1 2 100 4 5
// 10 20 0 40 50

可以ref 分配条件 ref 表达式的结果,将其用作引用返回,或将其作为 refoutinref readonly方法参数传递。 还可以分配条件 ref 表达式的结果,如前面的示例所示。

ref 条件表达式的语法如下所示:

condition ? ref consequent : ref alternative

条件 ref 表达式与条件运算符相似,仅计算两个表达式其中之一:consequentalternative

在 ref 条件表达式中,consequentalternative 的类型必须相同。 ref 条件表达式不由目标确定类型。

条件运算符和 if 语句

需要根据条件计算值时,使用条件运算符而不是 if 语句可以使代码更简洁。 下面的示例演示了将整数归类为负数或非负数的两种方法:

int input = new Random().Next(-5, 5);

string classify;
if (input >= 0)
{
    classify = "nonnegative";
}
else
{
    classify = "negative";
}

classify = (input >= 0) ? "nonnegative" : "negative";
运算符可重载性

用户定义类型不能重载条件运算符。

程序循环

迭代语句用于遍历集合(如数组),或重复执行同一组语句直到满足指定的条件。 有关详细信息,请参阅下列主题:

  • do
  • for
  • foreach
  • while

此迭代语句重复执行语句或语句块。 for 语句:在指定的布尔表达式的计算结果为 true 时会执行其主体。 foreach 语句:枚举集合元素并对集合中的每个元素执行其主体。 do 语句:有条件地执行其主体一次或多次。 while 语句:有条件地执行其主体零次或多次。

在迭代语句体中的任何点,都可以使用 break 语句跳出循环。 可以使用 continue 语句进入循环中的下一个迭代。

for 语句

在指定的布尔表达式的计算结果为 true 时,for 语句会执行一条语句或一个语句块。 以下示例显示了 for 语句,该语句在整数计数器小于 3 时执行其主体:

for (int i = 0; i < 3; i++)
{
    Console.Write(i);
}
// Output:
// 012

上述示例展示了 for 语句的元素:

  • “初始化表达式”部分仅在进入循环前执行一次。 通常,在该部分中声明并初始化局部循环变量。 不能从 for 语句外部访问声明的变量。

    上例中的“初始化表达式”部分声明并初始化整数计数器变量:

    int i = 0

  • “条件”部分确定是否应执行循环中的下一个迭代。 如果计算结果为 true 或不存在,则执行下一个迭代;否则退出循环。 “条件”部分必须为布尔表达式。

    上例中的“条件”条件部分检查计数器值是否小于 3:

    i < 3

  • “迭代器”部分定义循环主体的每次执行后将执行的操作。

    上例中的“迭代器”部分增加计数器:

    i++

  • 循环体,必须是一个语句或一个语句块。

迭代器”部分可包含用逗号分隔的零个或多个以下语句表达式:

  • increment 表达式添加前缀或后缀,如 ++ii++
  • decrement 表达式添加前缀或后缀,如 --ii--
  • assignment
  • 方法的调用
  • await表达式
  • 通过使用 new 运算符来创建对象

如果未在“初始化表达式”部分中声明循环变量,则还可以在“初始化表达式”部分中使用上述列表中的零个或多个表达式。 下面的示例显示了几种不太常见的“初始化表达式”和“迭代器”部分的使用情况:为“初始化表达式”部分中的外部变量赋值、同时在“初始化表达式”部分和“迭代器”部分中调用一种方法,以及更改“迭代器”部分中的两个变量的值:

int i;
int j = 3;
for (i = 0, Console.WriteLine($"Start: i={i}, j={j}"); i < j; i++, j--, Console.WriteLine($"Step: i={i}, j={j}"))
{
    //...
}
// Output:
// Start: i=0, j=3
// Step: i=1, j=2
// Step: i=2, j=1

for 语句的所有部分都是可选的。 例如,以下代码定义无限 for 循环:

for ( ; ; )
{
    //...
}

foreach 语句

foreach 语句为类型实例中实现 System.Collections.IEnumerableSystem.Collections.Generic.IEnumerable 接口的每个元素执行语句或语句块,如以下示例所示:

List<int> fibNumbers = [0, 1, 1, 2, 3, 5, 8, 13];
foreach (int element in fibNumbers)
{
    Console.Write($"{element} ");
}
// Output:
// 0 1 1 2 3 5 8 13

foreach 语句并不限于这些类型。 可以将其与满足以下条件的任何类型的实例一起使用:

  • 类型具有公共无参数 GetEnumerator 方法。 GetEnumerator 方法可以是类型的扩展方法
  • GetEnumerator 方法的返回类型具有公共 Current 属性和公共无参数 MoveNext 方法(其返回类型为 bool)。

下面的示例使用 foreach 语句,其中包含 System.Span 类型的实例,该实例不实现任何接口:

Span<int> numbers = [3, 14, 15, 92, 6];
foreach (int number in numbers)
{
    Console.Write($"{number} ");
}
// Output:
// 3 14 15 92 6

如果枚举器的 Current 属性返回引用返回值ref T,其中 T 为集合元素类型),就可以使用 refref readonly 修饰符来声明迭代变量,如下面的示例所示:

Span<int> storage = stackalloc int[10];
int num = 0;
foreach (ref int item in storage)
{
    item = num++;
}
foreach (ref readonly var item in storage)
{
    Console.Write($"{item} ");
}
// Output:
// 0 1 2 3 4 5 6 7 8 9

如果 foreach 语句的源集合为空,则 foreach 语句的正文不会被执行,而是被跳过。 如果 foreach 语句应用为 null,则会引发 NullReferenceException

await foreach

可以使用 await foreach 语句来使用异步数据流,即实现 IAsyncEnumerable 接口的集合类型。 异步检索下一个元素时,可能会挂起循环的每次迭代。 下面的示例演示如何使用 await foreach 语句:

await foreach (var item in GenerateSequenceAsync())
{
    Console.WriteLine(item);
}

还可以将 await foreach 语句与满足以下条件的任何类型的实例一起使用:

  • 类型具有公共无参数 GetAsyncEnumerator 方法。 该方法可以是类型的扩展方法
  • GetAsyncEnumerator 方法的返回类型具有公共 Current 属性和公共无参数 MoveNextAsync 方法(其返回类型为 TaskValueTask 或任何其他可等待类型,其 awaiter 的 GetResult 方法返回 bool 值)。

默认情况下,在捕获的上下文中处理流元素。 如果要禁用上下文捕获,请使用 TaskAsyncEnumerableExtensions.ConfigureAwait 扩展方法。 有关同步上下文并捕获当前上下文的详细信息,请参阅使用基于任务的异步模式。 有关异步流的详细信息,请参阅异步流教程

迭代变量的类型

可以使用 var 关键字让编译器推断 foreach 语句中迭代变量的类型,如以下代码所示:

foreach (var item in collection) { }

备注

译器可以将 var 的类型推断为可为空的引用类型,具体取决于是否启用可为空的感知上下文以及初始化表达式的类型是否为引用类型。 有关详细信息,请参阅隐式类型本地变量

还可以显式指定迭代变量的类型,如以下代码所示:

IEnumerable<T> collection = new T[5];
foreach (V item in collection) { }

在上述窗体中,集合元素的类型 T 必须可隐式或显式地转换为迭代变量的类型 V。 如果从 TV 的显式转换在运行时失败,foreach 语句将引发 InvalidCastException。 例如,如果 T 是非密封类类型,则 V 可以是任何接口类型,甚至可以是 T 未实现的接口类型。 在运行时,集合元素的类型可以是从 T 派生并且实际实现 V 的类型。 如果不是这样,则会引发 InvalidCastException

do 语句