Bootstrap

C#中的using关键字

一、前言

前段时间开始接触在VS下用C#开发(其实是WPF,只是说到VS,往往会扯上C#,而且WPF框架的后台语言确实主要是C#),新建一个WPF应用程序后,在MainWindow.xaml.cs文件开头会有一堆using xxx的语句。
在这里插入图片描述
因为以前接触过C++,所以对using也不陌生,毕竟using namespace std这样的语句在C++中非常常见。所以很自然的,也很想当然的认为这边的用法和C++一样,就是引入命名空间,不必深究。

不过,在最近学习EF Core的过程中,在上下文的使用时,看到了这样的语句,觉得很陌生:

using (var context = new MyContext ())
{
    ...
}

其实,这个用法的含义也不难猜,我当时觉得应该是跟context的作用域/生命周期有关,大概是想用完就销毁,不再占用资源。但是呢,知识点这种东西不能一直猜啊,万一猜错了呢,所以我决定还是系统地学习一下。

二、using学习

首先C#中的using是个关键字

啥是关键字呢?
一开始接触编程时,老师就有教,编程的时候,并不是所有的单词都能拿来命名用的,有一部分单词是保留的,
对编译器有着特殊含义的,那这些保留的、预定义的单词你可以认为就是关键字(但不绝对)。

using关键字有两个主要的用途:

  • using语句定义了一个作用域,在作用域的尾部将会释放对象
  • using指令为命名空间创建别名或导入其他命名空间中定义的类型

这边两个用途就对应于前言部分的两种情况。接下来将两种用途展开讲讲。

1. using指令

using指令允许你使用在命名空间中定义的类型,而无需指定该类型的全名(有点C系语言中include的味道)。using指令会导入命名空间中的所有类型,如下所示:

using System.Text;

还可以给using指令加上两个修饰符:

  • global,该修饰符与向项目中每个源文件添加相同的using指令具有相同的效果。该修饰符是在C# 10中引入的。
  • static,该修饰符从单个类型导入静态成员和嵌套类型,而不是导入所有类型。它是C#6.0中引入的。

你可以结合使用这两个修饰符,来达到从项目所有源文件中的类型中导入静态成员的目的。
你还可以使用using alias directive(别名指令)为命名空间或类型创建别名。

// using alias = namespace
using Project = PC.MyCompany.Project;

它也可以结合global来使用。

不带global修饰符的using指令的作用域是它所在的文件。using指令可以出现在以下位置:

  • 源码文件的开头,在任何命名空间或类型声明之前。(一般文件中开头一大堆using就是这么用的)
  • 在任意的命名空间中(即空间中套空间),但必须在该命名空间中声明任何命名空间或类型之前(空间中的开头部分),除非使用了global修饰符,在这种情况下,指令必须出现在所有命名空间和类型声明之前。

否则,会报编译错误CS1529。

建一个using指令来使用命名空间中的类型时,不必指定命名空间。
using指令不能访问嵌套在指定的命名空间中的任何命名空间。

比如 using x;只能访问x中直接包含的内容,
namespace x 中有 namespace y,那y命名空间中的东西就访问不了了。
需要你using x.y。

命名空间分两类:用户定义的和系统定义的。
用户定义的命名空间是在你的代码中定义的。系统定义的命名空间有许多,请参考.NET API Browser.

1.1. global修饰符

向using指令添加global修饰符表示将using应用于编译的所有文件(通常是整个项目)。
它的使用语法:

// fully-qualified-namespace可以理解为命名空间的全名
global using <fully-qualified-namespace>;

一个global using指令可以出现在任意源码文件的开头位置。在单个文件中的所有global using指令必须出现以下部分之前:

  • 所有没有global修饰符的using指令
  • 文件中所有命名空间和类型声明

一般在使用的时候,往往会将它们放在一个地方。global using指令的顺序并不重要。

global修饰符可以与static修饰符组合使用。global修饰符可以应用于using alias 指令。在这两种情况下,指令的作用域都是当前编译的所有文件。下面示例使用了System.Math中声明的所有方法:

global using static System.Math;

还可以通过添加<using>项,例如使用<Using Include=“My.Awesome.Namespace” />来包含一个命名空间。

.NET 6的C#模板使用顶级语句(top level statements)。如果你已经升级到.NET 6,那代码可能与本文的不匹配。

1.2. static修饰符

using static指令一个类型,该类型的static成员和嵌套类型无需指定类型名即可访问。
该指令在C# 6中引入,它的语法如下:

using static 全名;

using static指令适用于任何具有static成员(或嵌套类型)的类型,即使它也具有实例成员。不过,它的实例成员只能由类型实例来调用。

你可以访问某个类型的静态成员,而不需要指定类型名来访问。

using static System.Console;
using static System.Math;
class Program
{
    static void Main()
    {
        WriteLine(Sqrt(3*3 + 4*4));
    }
}

通常,在调用静态成员时,需要提供类型名和成员名。重复输入相同的类型名来调用该类型的成员会导致冗长、晦涩的代码。例如,下面的Circle类定义引用Math类的成员:

using System;

public class Circle
{
   public Circle(double radius)
   {
      Radius = radius;
   }

   public double Radius { get; set; }

   public double Diameter
   {
      get { return 2 * Radius; }
   }

   public double Circumference
   {
      get { return 2 * Radius * Math.PI; }
   }

   public double Area
   {
      get { return Math.PI * Math.Pow(Radius, 2); }
   }
}

通过消除每次引用成员时显式引用Math类,using static指令产生更精简、清晰的代码:

using System;
using static System.Math;

public class Circle
{
   public Circle(double radius)
   {
      Radius = radius;
   }

   public double Radius { get; set; }

   public double Diameter
   {
      get { return 2 * Radius; }
   }

   public double Circumference
   {
      get { return 2 * Radius * PI; }
   }

   public double Area
   {
      get { return PI * Pow(Radius, 2); }
   }
}

using static导入时,只能访问指定类型中静态成员和嵌套类型。继承的成员不导入。你可以用 using static指令从任何的命名类型导入,包括VB模块。如果F#顶级函数以指定类型的静态成员的形式出现在元数据中,该类型的名称是有效的C#标识符,那么F#函数也能被导入。

using static使在指定类型中声明的扩展方法可用于扩展方法查找。但是,扩展方法的名称不会被导入到代码中非限定引用范围中。

从不同的类型中导入相同名称的方法,这些类型在同一编译单元或命名空间中使用不同的using static指令导入的,形成一个方法组(这tm原句妥妥的考研英语长难句,定语好几个,翻译出来贼拗口,反正就是指不同命名空间中方法重名的情况)。这些方法组中的重载解析遵循正常的C#规则。

下列示例使用using static指令使Console、Math和String类的静态成员可用,而无需指定它们的类型名称:

using System;
using static System.Console;
using static System.Math;
using static System.String;

class Program
{
   static void Main()
   {
      Write("Enter a circle's radius: ");
      var input = ReadLine();
      if (!IsNullOrEmpty(input) && double.TryParse(input, out var radius)) {
         var c = new Circle(radius);

         string s = "\nInformation about the circle:\n";
         s = s + Format("   Radius: {0:N2}\n", c.Radius);
         s = s + Format("   Diameter: {0:N2}\n", c.Diameter);
         s = s + Format("   Circumference: {0:N2}\n", c.Circumference);
         s = s + Format("   Area: {0:N2}\n", c.Area);
         WriteLine(s);
      }
      else {
         WriteLine("Invalid input...");
      }
   }
}

public class Circle
{
   public Circle(double radius)
   {
      Radius = radius;
   }

   public double Radius { get; set; }

   public double Diameter
   {
      get { return 2 * Radius; }
   }

   public double Circumference
   {
      get { return 2 * Radius * PI; }
   }

   public double Area
   {
      get { return PI * Pow(Radius, 2); }
   }
}
// The example displays the following output:
//       Enter a circle's radius: 12.45
//
//       Information about the circle:
//          Radius: 12.45
//          Diameter: 24.90
//          Circumference: 78.23
//          Area: 486.95

在本例中,using static指令也可以用于Double类型。添加该指令后,可以使得调用TryParse(String, Double)方法而不需要指定类型名。但是,使用不带类型名的TryParse会导致代码的可读性变差,因为需要检查using static指令以确定调用了哪个数字类型的TryParse方法。

using static也可以应用于enum类型(枚举类型)。通过对enum添加using static引用,该枚举类型不再需要使用enum成员的方式。

using static Color;

enum Color
{
    Red,
    Green,
    Blue
}

class Program
{
    public static void Main()
    {
        Color color = Green;
    }
}

△1.3. 使用别名

创建using 别名指令,来使标识符更易被识别(用你自己熟悉的名称)。在任何using指令中,必须使用完全限定的名称空间或类型,无论在它之前有哪些using指令。无法在using指令的声明中使用using别名。例如,以下示例会产生一个编译错误:

using s = System.Text;
using s.RegularExpressions; // Generates a compiler error.

以下示例展示了如何为一个命名空间定义并使用一个using别名:

namespace PC
{
    // Define an alias for the nested namespace.
    using Project = PC.MyCompany.Project;
    class A
    {
        void M()
        {
            // Use the alias
            var mc = new Project.MyClass();
        }
    }
    namespace MyCompany
    {
        namespace Project
        {
            public class MyClass { }
        }
    }
}

using别名指令不能在其右侧有开放的泛型类型(就是未明确定义类型的)。例如,你无法为List<T>创建别名,但你可以为List<int>创建别名。

以下示例展示了如何为一个类定义一个using指令和一个using别名:

using System;

// Using alias directive for a class.
using AliasToMyClass = NameSpace1.MyClass;

// Using alias directive for a generic class.
using UsingAlias = NameSpace2.MyClass<int>;

namespace NameSpace1
{
    public class MyClass
    {
        public override string ToString()
        {
            return "You are in NameSpace1.MyClass.";
        }
    }
}

namespace NameSpace2
{
    class MyClass<T>
    {
        public override string ToString()
        {
            return "You are in NameSpace2.MyClass.";
        }
    }
}

namespace NameSpace3
{
    class MainClass
    {
        static void Main()
        {
            var instance1 = new AliasToMyClass();
            Console.WriteLine(instance1);

            var instance2 = new UsingAlias();
            Console.WriteLine(instance2);
        }
    }
}
// Output:
//    You are in NameSpace1.MyClass.
//    You are in NameSpace2.MyClass.

△1.4. 如何使用VB My命名空间


VB部分我大概是不会用到的。

2. using语句

提供了便捷的语法来确保正确使用IDisposable对象。从C# 8.0开始,using语句还保证IAsyncDisposable对象的正确使用。

???
我相信初学者看到这一脸懵逼,IDisposable是啥?IAsyncDisposable又是啥?
跟using语句有啥关系啊?
我看到这也是这种感觉,mdzz
但微软文档介绍using语句的时候,就是这么写的,没办法,只能先将IDisposable简单学习下。

2.1. IDisposable接口

IDisposable从命名来看就是个接口。
Disposable:可任意使用的、一次性的、用完即丢弃的。
该接口的定义是,提供了一种释放非托管资源的机制

IDisposable的文档部分非常直白地介绍了该接口的用途:
此接口的主要用途是释放非托管资源
当管理的对象不再使用时,GC(.NET平台的垃圾回收器)会自动释放分配给该对象的内存。但是呢,到底何时回收垃圾是无法被预测的。而且,GC也不了解非托管资源,如窗口句柄、或打开的文件和流。
使用此接口的Dispose方法可以用GC显式地释放非托管的资源。当某个对象不再被需要时,对象的使用者可以调用此方法。

那么使用实现了IDisposable接口的对象通常有两种方式,
其中一种就是使用一种语言结构,如C#和VB中的using语句,以及F#中的use语句或using函数。当环境支持该语言结构时,你可以使用它来代替显式地调用IDisposable.Dispose。

重点看一下C#中的using语句,下面示例用这种方法定义了一个WordCount类,它保留了关于文件和单词数量的信息:

using System;
using System.IO;
using System.Text.RegularExpressions;

public class WordCount
{
    private String filename = String.Empty;
    private int nWords = 0;
    private String pattern = @"\b\w+\b";

    public WordCount(string filename)
    {
        if (!File.Exists(filename))
            throw new FileNotFoundException("The file does not exist.");

        this.filename = filename;
        string txt = String.Empty;
        using (StreamReader sr = new StreamReader(filename))
        {
            txt = sr.ReadToEnd();
        }
        nWords = Regex.Matches(txt, pattern).Count;
    }

    public string FullName
    { get { return filename; } }

    public string Name
    { get { return Path.GetFileName(filename); } }

    public int Count
    { get { return nWords; } }
}

using语句实际上是一种语法上的便利(语法糖?)。在编译时,语言编译器为try/finally块实现了中间语言(IL)。

好,现在IDisposable部分到此打住了,可以大致知道using语句其实是隐式地用了IDisposable.Dispose。对不用的对象进行了清理(新手理解到这层应该可以了)。

2.2. 示例

现在,回过来看using语句的示例:

string manyLines = @"This is line one
This is line two
Here is line three
The penultimate line is line four
This is the final, fifth line.";

using (var reader = new StringReader(manyLines))
{
    string? item;
    do
    {
        item = reader.ReadLine();
        Console.WriteLine(item);
    } while (item != null);
}

在C# 8.0引入的using声明是不需要大括号的(我个人觉得还是加了大括号可读性强一点,作用域给标注了嘛):

string manyLines = @"This is line one
This is line two
Here is line three
The penultimate line is line four
This is the final, fifth line.";

using var reader = new StringReader(manyLines);
string? item;
do
{
    item = reader.ReadLine();
    Console.WriteLine(item);
} while (item != null);

2.3. 附注

File和Font是访问非托管资源(这里指的是文件句柄和设备上下文)的托管类型的例子。还有许多其他类型的非托管资源和封装它们的类库类型。所有这些类型都必须实现IDisposable接口,或者IAyncDisposable接口。

当IDisposable对象的生命周期被限制在单个方法时,你应该在using语句或using声明中声明并实例化它。这句话我觉得就是using语句使用的精髓了,就是using中把你需要限制生命周期的对象给加进去,那么在你大括号框的那段区域就是它的活动空间,超出区域就被回收了。using声明在对象超出作用域时,以正确的方式调用对象的Dispose方法。一旦调用Dispose,using语句就会造成对象本身超出作用域。在using块中,该对象是只读的,不能修改或重新分配。使用using声明声明的变量是只读的。如果对象实现了IAsyncDisposable接口而不是IDisposable,则using 结构调用DisposeAsync并等待返回的ValueTask。

两种using结构都会保证即使在using块中发生异常也会调用Dispose(或DisposeAsync)。你可以通过将对象放在try块中,然后在finally块中调用Dispose(或DisposeAsync)来实现相同的效果。实际上,编译器就是这样转换using语句和using声明的。上面的代码示例在编译时会展开为以下代码(注意,额外的花括号为对象创建限制范围):

string manyLines = @"This is line one
This is line two
Here is line three
The penultimate line is line four
This is the final, fifth line.";

{
    var reader = new StringReader(manyLines);
    try
    {
        string? item;
        do
        {
            item = reader.ReadLine();
            Console.WriteLine(item);
        } while (item != null);
    }
    finally
    {
        reader?.Dispose();
    }
}

较新的using语句语法转变成了类似的写法。try块在声明变量的地方打开。finally块添加在封闭块的末尾,通常是方法的末尾。

一个类型的多个实例也可以在一个using语句中声明,如下示例所示。要注意的是,当你在一条语句中声明多个变量时,你不能使用隐式类型变量(var):

string numbers = @"One
Two
Three
Four.";
string letters = @"A
B
C
D.";

using (StringReader left = new StringReader(numbers),
    right = new StringReader(letters))
{
    string? item;
    do
    {
        item = left.ReadLine();
        Console.Write(item);
        Console.Write("    ");
        item = right.ReadLine();
        Console.WriteLine(item);
    } while (item != null);
}

你也可以使用C# 8中引入的声明语法来组合相同类型的多个声明,如下所示:

string numbers = @"One
Two
Three
Four.";
string letters = @"A
B
C
D.";

using StringReader left = new StringReader(numbers),
    right = new StringReader(letters);
string? item;
do
{
    item = left.ReadLine();
    Console.Write(item);
    Console.Write("    ");
    item = right.ReadLine();
    Console.WriteLine(item);
} while (item != null);

你可以实例化资源对象,然后将变量传给using语句,但这不是最好的使用方式。该情况下,在控制离开using块之后,对象仍然在范围内,但可能无法访问其非托管资源。换句话说,它没有完全初始化。如果你试图在using块外使用对象,就有引发异常的风险。因为这个原因,最好在using语句中实例化对象,并将其作用域限制在using块。

string manyLines = @"This is line one
This is line two
Here is line three
The penultimate line is line four
This is the final, fifth line.";

var reader = new StringReader(manyLines);
using (reader)
{
    string? item;
    do
    {
        item = reader.ReadLine();
        Console.WriteLine(item);
    } while (item != null);
}
// reader is in scope here, but has been disposed

三、总结

C#中的using有两种用法,

  • using指令用于引入命名空间。
  • using语句用于便捷地使用实现了IDisposable接口的对象,使之生命周期限制在指定的作用域内。
;