文章目录
一、前言
前段时间开始接触在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接口的对象,使之生命周期限制在指定的作用域内。