C#学习笔记04之语句

程序操作使用语句进行表示。C#支持几种不同的语句,其中许多语句是从嵌入语句的角度来定义的。

  • 代码块:由一系列在分隔符{}内编写的语句组成,可以包含一个或多个语句。当代码块只包含一个语句时,可以省略{}
  • 声明语句:用于声明局部变量和常量。
  • 表达式语句:用于计算表达式,包括方法调用、使用new运算符的对象分配、赋值、递增和递减运算等。
  • 选择语句:用于根据一些表达式的值从多个可能的语句中选择一个以供执行,如ifif-elseswitch
  • 循环语句:用于重复执行嵌入语句,如whiledo-whileforforeach
  • 跳转语句:用于转移控制权,如breakcontinuereturngotothrowyield
  • 异常处理语句:用于捕获并处理在代码块执行期间发生的异常,如trycatchfinally
  • 溢出检查语句用于控制整型类型算术运算和转换的溢出检查上下文,如checkedunchecked
  • unsafe语句用于声明不安全的上下文,主要用于操作指针。
  • fixed语句用于防止垃圾收集器重新定位可移动变量。
  • lock语句用于获取给定对象的相互排斥锁定,执行语句,然后解除锁定。
  • using语句用于获取资源,执行语句,然后释放资源。
  • yield语句用于迭代器以提供下一个值或表示迭代结束。

选择语句

选择语句也叫分支语句,可以根据表达式的值从许多可能的路径中选择要执行的语句。if语句根据布尔表达式的值来选择要执行的语句,switch语句根据与表达式匹配的模式在语句列表中选择要执行的语句。

if

if语句有两种形式:不包含else部分的if语句仅在布尔表达式计算结果为true时执行其主体;包含else部分的if语句根据布尔表达式的值选择两个语句中的一个来执行。

if (condition) { consequent; }

if (condition) { consequent; }
else if (condition) { alternative; }
else { alternative; }

简单的if else语句可以使用?:运算符代替以简化代码。可嵌套if语句来检查多个条件。

switch

switch语句根据与表达式的模式在语句列表中选择要执行的语句。switch语句按文本顺序从上到下对case进行匹配,若匹配成功,并且匹配约束(case guard)为true(如果存在的话),则进入相应casedefault指定匹配表达式与其他任何case都不匹配时要执行的语句。default可以在switch的任何位置,但不管在哪个位置,default总是最后计算,并且仅在其他所有case都不匹配时计算。如果匹配表达式与任何case都不匹配,且没有default,控制就会贯穿switch语句。

匹配约束为与case匹配的附加条件。匹配约束必须是布尔表达式,在case后面的when关键字之后指定。

switch ((int)score / 10)
{
    case 10 when score == 100f:
    case 9:
        Console.WriteLine("Great!");
        break;
    case 8:
        Console.WriteLine("Good!");
        break;
    case 7:
    case 6:
        Console.WriteLine("Qualified!");
        break;
    case 5:
    case 4:
    case 3:
    case 2:
    case 1:
    case 0:
        Console.WriteLine("Failed!");
        break;
    default:
        Console.WriteLine("Wrong Input!");
        break;
}

编译器在switch语句包含无法访问的case时会生成错误。可以为switch语句的一部分指定多个case(即控制可以从一个case贯穿到下一个case),但通常使用break语句将控制从case语句传递出去。还可使用returngotothrow语句将控制从case语句传递出去。

switch语句支持整型、浮点、字符、字符串、枚举,从C# 7.0开始支持模式匹配。

循环语句

循环语句用于重复执行一条语句或代码块,通常包括四个部分:声明并初始化循环控制变量、循环条件表达式、循环体、迭代操作

  • 通常需要声明并初始化循环控制变量。
  • 循环条件表达式是一个布尔表达式,用于确定是否应执行循环中的下一个迭代。若该表达式计算结果为true或不存在,执行下一个迭代,否则退出循环。
  • 循环体是循环执行的操作,可包含一条语句或一个代码块。
  • 迭代操作是在每此循环体执行后将执行的操作,用于改变循环控制变量从而控制是否该退出循环。

在循环语句里循环体中的任何位置都可以使用break语句中断循环,或者使用continue语句继续执行循环中的下一次迭代

while

在循环条件表达式的计算结果为true时,while语句会执行循环体。由于在每次计算此表达式之后才执行循环体,所以while循环体会执行0次或多次。 应在while循环体中改变循环控制变量。

int i = 0;
while (i < 5)
{
    i++;
}

do-while

在循环条件表达式的计算结果为true时,do-while语句会执行循环体。由于在每次计算此表达式之前都会执行循环体,所以do-while循环体至少会执行1次。 应在do-while循环体中改变循环控制变量。

int i = 0;
do
{
    i++;
}while (i < 5);

for

在循环条件表达式的计算结果为true时,for语句会执行循环体。

  • 声明并初始化循环控制变量可以在for语句的初始化表达式中进行,该初始化仅在进入循环前执行一次。此时该循环控制变量无法从for语句外部访问。
  • 迭代操作不仅可以包括对循环控制变量的操作,还可包含用逗号分隔的零个或多个其他语句表达式。
for (int i = 0, b = 0; i < 2; i++, b++)
{
    // 循环体
}

for语句的所有部分都是可选的。

// 死循环
for { ; ; }
{
    // 循环体
}

foreach

foreach语句通常用于从头至尾循环读取数据集合中的所有子项而不需要循环控制变量、循环条件表达式和迭代操作, 如遍历数组等。

int[] array = new int[10];
foreach (int element in array)
{
    // 循环体
}

以下类型可以使用foreach语句遍历:

  • 实现了System.Collections.Generic.IEnumerableSystem.Collections.Generic.IEnumerable<T>接口。
  • 具有公共无参的GetEnumerator()方法(可以是扩展方法),并且该方法的返回值具有公共Current属性和公共无参且返回值为bool类型的MoveNext方法。

如果foreach语句读取到的引用为null,则会引发NullReferenceException错误。如果foreach语句的源集合为空,则foreach语句的循环体不会被执行,而是被跳过。

foreach语句只能读取,无法改变源集合中的数据。 读取的子项item需要指定类型,显示指定类型时,源集合中数据类型必须可以隐式或显示的转换为指定类型,否则将引发InvalidCastException错误;可以使用var关键字推断类型。

原理:foreach首先会获得源集合中的迭代器(因此源集合需要具有迭代器才能使用foreach遍历),在循环中通过迭代器的MoveNext()移动到下一个元素,然后获取元素Current,执行操作。

// 获取迭代器
IEnumerator iterator = 源集合.GetEnumerator();
// 移动到下一个元素
while (iterator.MoveNext())
{
    // 获取元素
    var item = iterator.Current;
    // 其他操作
}

跳转语句

跳转语句可以无条件地转移程序控制。

break

break终止最近的封闭循环语句(即forforeachwhiledo-while)或switch语句,并将控制权转交给已终止语句后面的语句(若有)。

在嵌套循环中,break仅终止包含它的最内部循环。在循环内使用switch语句时,break仅从switch语句中转移控制权,包含switch的循环不受影响。

continue

continue中止当前轮的循环,开始最近的封闭循环语句(即forforeachwhiledo-while)新一轮的循环。

return

return终止其所在的方法,并将控制权和方法结果(若有)返回给调用方。如果方法无返回值,则使用不带表达式的return语句,无return语句时在执行完最后一条语句后终止。

如果return语句具有表达式,该表达式必须可隐式转换为函数成员的返回类型,除非它是异步的。对于async函数,表达式必须可隐式转换为Task<TResult>ValueTask<TResult>类型,以函数的返回类型为准。如果async函数的返回类型为TaskValueTask,则使用不带表达式的return语句。

默认情况下,return语句返回表达式的值。从C# 7.0开始,可以使用带ref关键字的return语句返回对变量的引用。

ref int FindFirst(int[] numbers, Func<int, bool> predicate)
{
    for (int i = 0; i < numbers.Length; i++)
    {
        if (predicate(numbers[i]))
        {
            return ref numbers[i];
        }
    }
    throw new InvalidOperationException("No element satisfies the given condition.");
}

goto

goto将控制权转交给带有指定标签的语句。

int i = 0;
if (i > 5)
{
    goto Condition;
}
else
{
    // 操作
}

Condition:
// 操作

可以使用goto语句退出循环或在switch语句中将控制权移交到具有常量标签的case,还可使用语句goto default;将控制权转交给default

int choice = 1;
int result = 0;
switch (choice)
{
    case 0:
        result += 0;
        break;
    case 1:
        result += 1;
        goto case 0;
    case 2:
        result += 2;
        goto default;
    default:
        result += 10;
        break;
}

如果当前函数成员中不存在具有给定名称的标签,或者goto语句不在标签范围内,则会出现编译时错误,即不能使用goto语句将控制权从当前函数成员转移到任何嵌套范围之外的地方。

异常处理语句

使用try语句在可能出现异常的地方捕获异常,catch语句处理异常,finally语句释放资源,throw关键字引发异常。

throw

引发程序执行期间出现异常的信号。然后方法调用方使用try-catchtry-catch-finally块来处理引发的异常。

// 语法
throw [e]; // e是一个派生自System.Exception类的实例

public class NumberGenerator
{
   int[] numbers = { 2, 4, 6, 8, 10, 12, 14, 16, 18, 20 };
   public int GetNumber(int index)
   {
      if (index < 0 || index >= numbers.Length)
      {
         throw new IndexOutOfRangeException();
      }
      return numbers[index];
   }
}

throw也可以用于catch块,以重新引发在catch块中处理的异常。此时,throw不使用异常操作数。

public char GetFirstCharacter()
{
    try
    {
        return Value[0];
    }
    catch (NullReferenceException e)
    {
        throw;
    }
}

从C# 7.0开始,throw可以用作表达式和语句,这允许在上下文中引发异常。

// 在C# 7.0之前,此逻辑需在if/else中实现
string arg = args.Length >= 
    1 ? args[0] : throw new ArgumentException("You must supply an argument");

try-catch

try-catch语句为后接一或多个catch子句的try块,这些子句指定不同异常的处理程序。try块包含可能导致异常的受保护的代码。

引发异常时,公共语言运行时(CLR)查找处理此异常的catch语句。如果当前正在执行的方法不包含此类catch块,则CLR查看调用了当前方法的方法,并以此类推遍历调用堆栈。如果未找到任何catch,则CLR向用户显示一条未处理的异常消息,并停止执行程序。

不推荐使用不带参数catch子句来捕获任何类型的异常。通常只应捕获知道如何从其恢复的异常。 因此,应始终指定派生自System.Exception的对象参数。异常类型应尽可能具体,以避免不正确地接受异常处理程序实际上无法解决的异常。因此,最好是在Exception基类型上使用具体的异常。

// 处理异常
int[] gen = new int[] { 1 };
try
{
    int value = gen.GetNumber(index);
    Console.WriteLine($"Retrieved {value}");
}
catch (IndexOutOfRangeException e)
{
    Console.WriteLine($"{e.GetType().Name}: {index} is outside the bounds of the array");
}
// Output: IndexOutOfRangeException: 10 is outside the bounds of the array

可以使用同一try-catch语句中的多个特定catch子句。在这种情况下,catch子句的顺序很重要,因为catch子句是按顺序检查的。在使用更笼统的子句之前获取更细节的异常。如果catch块的排序使得永不会达到后面的catch块,则编译器将产生错误。

try
{
    string s = null;
    ProcessString(s);
}
// Most specific:
catch (ArgumentNullException e)
{
    Console.WriteLine("{0} First exception caught.", e);
}
// Least specific:
catch (Exception e)
{
    Console.WriteLine("{0} Second exception caught.", e);
}

筛选想要处理的异常的一种方式是使用catch参数。也可以使用异常约束进一步检查该异常以决定是否要对其进行处理。如果异常约束返回false,则继续搜索处理程序。

catch (InvalidCastException e)
{
    if (e.Data == null)
    {
        throw;
    }
    else
    {
        // Take some action.
    }
}
// 等价于
catch (InvalidCastException e) when (e.Data != null)
{
    // Take some action.
}

异常约束要优于捕获和重新引发(如下所述),因为约束将保留堆栈不受损坏。如果之后的处理程序转储堆栈,可以查看到异常的原始来源,而不只是重新引发它的最后一个位置。异常约束表达式的一个常见用途是日志记录。可以创建一个始终返回false并输出到日志的异常约束,能在异常通过时进行记录,且无需处理并重新引发它们。

可在catch块中使用throw语句以重新引发已由catch语句捕获的异常。此时,throw不使用异常操作数。可以捕获一个异常而引发一个不同的异常。执行此操作时,请指定作为内部异常捕获的异常。

catch (InvalidCastException e)
{
    // Perform some action here, and then throw a new exception.
    throw new YourCustomException("Put your error message here.", e);
}

try块内,仅能初始化在其内部声明的变量;否则,在完成执行块之前,可能会出现异常。

try-finally

通过使用finally块,可以清除try块中分配的任何资源,即使在try块中发生异常,也可以运行代码。通常情况下,finally块的语句会在控制离开try语句时运行。

已处理的异常中会保证运行相关联的finally块。但是,如果异常未经处理,则finally块的执行将取决于异常解除操作的触发方式。反过来,这又取决于计算机的设置方式。只有在finally子句不运行的情况下,才会涉及程序被立即停止的情况。

通常情况下,当未经处理的异常终止应用程序时,finally块是否运行已不重要。但是,如果finally块中的语句必须在这种情况下运行,则可以将catch块添加到try-finally语句。另一种解决方法是,可以捕获可能在调用堆栈上方的try-finally语句的try块中引发的异常。可以通过以下几种方法来捕获异常:调用包含try-finally语句的方法、调用该方法或调用堆栈中的任何方法。如果未捕获异常,则finally块的执行取决于操作系统是否选择触发异常解除操作。

try-catch-finally

使用try-catch-finally语句来处理在try块执行期间可能发生的异常,并指定当控制离开try语句时必须执行的代码。当异常由catch块处理时,finally块在该catch块执行之后执行(即使在执行catch块期间发生另一个异常)。

溢出检查语句

checkedunchecked语句用于控制整型类型算术运算和转换的溢出检查上下文。在unchecked的代码块中,会自动丢弃不适合目标类型的任何高阶位来截断操作结果。在checked代码块中则会引发编译时异常(compile-time)。

uint a = uint.MaxValue;

unchecked
{
    Console.WriteLine(a + 3);  // output: 2
}

double b = double.MaxValue;
try
{
    a = checked((int)b);
}
catch (OverflowException e)
{
    Console.WriteLine(e.Message);  // output: Arithmetic operation resulted in an overflow.
}

checkedunchecked语句仅对当前语句块或强转括号内的操作执行溢出检查。

int Multiply(int a, int b) => a * b;
int factor = 2;

try
{
    checked
    {
        // 不检查其他地方的溢出
        Console.WriteLine(Multiply(factor, int.MaxValue));  // output: -2
    }
}
catch (OverflowException e)
{
    Console.WriteLine(e.Message);
}

try
{
    checked
    {
        // 仅对当前语句块或强转括号内的操作执行溢出检查
        Console.WriteLine(Multiply(factor, factor * int.MaxValue));
    }
}
catch (OverflowException e)
{
    Console.WriteLine(e.Message);  // output: Arithmetic operation resulted in an overflow.
}

其他语句

unsafe语句表示涉及指针操作的不安全的上下文。可以在类型或成员的声明中使用不安全修饰符,此时类型或成员的整个作用域都会被认为是不安全的上下文;也可以使用unsafe语句在该块中使用不安全代码。

unsafe static void FastCopy(byte[] src, byte[] dst, int count)
{
    // 不安全的上下文:从参数列表到代码块结束
}

unsafe
{
    // 不安全的上下文
}

fixed语句防止垃圾收集器重新定位可移动变量,并声明指向该变量的指针。固定或固定变量的地址在语句执行期间不会改变。只能在相应的固定语句中使用声明的指针。声明的指针是只读的,不能修改。使用stackalloc表达式在栈上分配的内存不受垃圾收集器的控制因此不需要fixed

unsafe
{
    byte[] bytes = [1, 2, 3];
    fixed (byte* pointerToFirst = bytes)
    {
        // 操作
    }
}

lock语句能够获取给定对象(引用类型)的互斥锁并执行语句块,然后释放锁。当锁被持有时,持有该锁的线程可以再次获取并释放该锁,而任何其他线程都被阻止获取锁并等待直到锁被释放。lock语句确保在任何时刻最多只有一个线程执行它的线程体。

lock (x)
{
    // 操作
}

// 等价于

object _lockObj = x;
bool _lockWasTaken = false;
try
{
    System.Threading.Monitor.Enter(_lockObj, ref _lockWasTaken);
    // 操作
}
finally
{
    if (_lockWasTaken) System.Threading.Monitor.Exit(__lockObj);
}

using语句确保正确使用IDisposable释放实例。当程序控制离开using语句时,即使在该语句内发生异常,也可以确保指定实例在其作用域结束时能够被释放。由using语声明的变量(可以同时声明多个,释放顺序与声明顺序相反)是只读的,不能对其重新赋值或将其作为ref/out参数传递。

string filePath = "";
using (StreamReader reader = File.OpenText(filePath))
{
    // 使用资源
}

迭代器中使用yield语句来提供下一个值(yield return)或表示迭代结束(yield break)。当迭代器开始迭代时,执行到第一个yield return语句,然后挂起迭代器并将第一个迭代值返回调用者处理;在随后的每次迭代中,迭代器从之前挂起的yield return语句之后继续执行,并一直到下一个yield return语句;当控制到达迭代器末尾或yield break语句时,迭代终止。

var numbers = ProduceEvenNumbers(5);
foreach (int i in numbers)
{
    // 操作
}

IEnumerable<int> ProduceEvenNumbers(int upto)
{
    for (int i = 0; i <= upto; i += 2)
    {
        yield return i;
    }
}
  • 25
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值