C# 中的 using 关键字

using 关键字的常见与不常见用法

由 Anawaert 于 2024-06-02 发布   

概述

  在C#编程中,using关键字常被用于导入命名空间、静态类与对非托管资源的释放。但是除了上述常见的几种用法外,using关键字还有一些其它的用法,本文将列举三个与using关键字有关的相对而言不常见的用法。

常见用法

导入命名空间

  类比C++,为了省去一长串的命名空间书写,C#使用using关键字在代码开头导入其它的命名空间,但不需要加上namespace关键字,常见的有:

using System;  // 导入System命名空间
using System.IO;  // 导入与IO相关的System.IO命名空间

  在导入命名空间以后,我们就不需要加上繁杂的命名空间来约束范围了。假设,在没有导入命名空间前,我们有一个用于读取文本文件内容并输出到控制台的程序代码需要这样写:

System.IO.StreamReader reader = new System.IO.StreamReader("input.txt");  // 创建StreamReader对象以读取文本
string text = reader.ReadToEnd();  // 从头到尾读取并赋值给text变量
System.Console.WriteLine(text);  // 在控制台打印
reader.Dispose();  // 释放资源

  在导入上述两个命名空间后,就可以简化为下面的代码:

StreamReader reader = new StreamReader("input.txt");  // 创建StreamReader对象以读取文本
string text = reader.ReadToEnd();  // 从头到尾读取并赋值给text变量
Console.WriteLine(text);  // 在控制台打印
reader.Dispose();  // 释放资源

  在不引起歧义的情况下,这样看起来是不是简洁一些呢?

导入静态类

  在C# 6.0中,C#提供了一种新的语法糖,即可以使用using关键字与static关键字在代码开头导入其它的静态类。以静态类System.Console为例,在代码开头,如果我们使用:

using static System.Console;

  那么,如果想把"Anawaert"输出到控制台,我们可以直接使用WriteLine("Anawaert");来完成,无需加上Console类的限定。

  细心的读者可能已经发现了,如果出现两个及以上被我们导入的静态类中,都有同名函数该怎么办?答案很简单,就是使用最原始的方法,使用完全的类名+函数名,否则编译系统也无法确定你想使用的哪一个函数。

释放非托管资源

  非托管资源指的是不受.NET垃圾回收器控制的资源,包括但不限于文件资源、数据库资源与其它非.NET实现的程序/库资源等。在上面的代码示例中,我们使用了一个StreamReader对象来读取文件内容,与它相关的就是非托管资源,因此,在最后我们需要使用该StreamReader对象的Dispose()方法将资源释放。

  由于非托管资源只在调用Dispose()方法后才会被释放,那么释放的时机就很重要,过早会导致资源无法使用,过晚又会造成内存资源被占用;不释放是最危险的,会造成内存溢出,直至程序生命周期结束。那么,是否有一些简单的方法可以让我们不用考虑在何时释放非托管资源呢?

  我们可以使用using语句代替手动调用Dispose()方法。同样还是上述的示例:

StreamReader reader = new StreamReader("input.txt");  // 创建StreamReader对象以读取文本
string text = reader.ReadToEnd();  // 从头到尾读取并赋值给text变量
Console.WriteLine(text);  // 在控制台打印
reader.Dispose();  // 释放资源

  可以改成:

using (StreamReader reader = new StreamReader("input.txt"))  // 创建StreamReader对象以读取文本
{
    string text = reader.ReadToEnd();  // 从头到尾读取并赋值给text变量
    Console.WriteLine(text);  // 在控制台打印
}

  是不是也更加简洁、方便呢?

  从C# 8.0起,我们甚至可以使用更加简洁的using语句来实现一样的功能:

using StreamReader reader = new StreamReader("input.txt");  // 创建StreamReader对象以读取文本
string text = reader.ReadToEnd();  // 从头到尾读取并赋值给text变量
Console.WriteLine(text);  // 在控制台打印

  在非托管资源对象的作用域明确时,这样的语法糖极大地方便了开发者,让开发者们无需关心资源是否已被释放的问题。

相对而言不常见的用法

为命名空间起别名

  C#允许为命名空间起别名,尤其是对于那些名称相当长,或者是有与其它命名空间成员重名情况的命名空间时。如正则表达式的命名空间System.Threading,我们可以使用下面的方法来指定别名:

using SysTrd = System.Threading;

  在使用别名时,可以使用.运算符来引出成员,也可以使用像C++中的::运算符来引出类成员:

SysTrd.Timer timer1 = new SysTrd.Timer((state) => { }, null, 0, 1000);  // 使用.运算符
SysTrd::Timer timer2 = new SysTrd::Timer((state) => { }, null, 0, 1000);  // 使用::运算符

  由于System.Timers命名空间下也有一个Timer类,因此使用别名+范围即可简化代码,并且不引起歧义。

为类型起别名

  在C# 12.0中,C#更新了一个新功能,这个新功能就是“类型别名”。这允许我们在C#中通using关键字,像C/C++中使用typedef关键字一样为类型指定别名,用法如下:

using str = string;  // str就是string类型
using ScoreArray = int[];  // “分数数组”类型是int型的数组
using ListOfIntList = System.Collections.Generic.List<System.Collections.Generic.List<int>>;  // “int型列表的列表”是List<List<int>>类型

  在指定别名后,我们就可以使用它们的别名来代替类型全名。但别名指定并不是随意的,有以下问题需要注意:

  • 不得与已有类型重名
  • 泛型别名必须指定泛型参数中的具体类型,不允许使用占位符
  • 为指针等类型指定别名时需要注意安全问题

全局导入命名空间、静态类型引用与指定别名

  有时候,我们的项目所有代码都会使用一些特定的命名空间,如System,或者是System.Text等。项目规模小还好,但是项目规模一旦大起来,那么为每一个源代码文件都写上using ...是十分麻烦且低效重复的。因此,C#允许我们在using关键字前使用global修饰符,表示在当前项目的所有源文件中导入命名空间或静态类,甚至还可以全局指定别名:

global using System.Text;  // 全局导入System.Text
global using static System.Math;  // 全局导入System.Math类
global using ListOfIntList = System.Collections.Generic.List<System.Collections.Generic.List<int>>;  // 全局指定别名

  但需要注意的是,由global修饰的using语句必须在所有其它using语句之前,即必须先全局导入,才能局部导入。下面这个示例是不合法的:

using static System.Console;
global using System.IO;  // 不合法,必须写在没有global修饰的using之前

总结

  C#using关键字可以提供(全局)导入命名空间、静态类、命名空间/类型别名、释放非托管资源的功能,用法灵活,相关语法简洁,尤其是与释放非托管资源相关的using语句用法,有不少有意思的技术问题与陷阱,欢迎大家前去学习、探索。若有关于using关键字的其它各种热、冷知识,欢迎大家在文章下方的评论区补充留言。