不定时更新翻译系列,此系列更新毫无时间规律,文笔菜翻译菜求各位看官老爷们轻喷,如觉得我翻译有问题请挪步原博客地址

本博文翻译自:
http://www.dotnetcurry.com/csharp/1411/csharp-favorite-features

在这篇文章中,请您和我一起浏览C#的各种版本,并分享每个版本中我最喜欢的特性。我将在强调实用性的同时展示其优点。

C#我最喜欢的功能 - V1至V7

C#1.0版本

C#1.0版本(ISO-1)真的是一种非常无趣的东西,没有什么特别令人兴奋的东西,而且它缺少很多开发者喜欢的语言。然而,有一种特别的特征,我认为是我最喜欢的。- 隐式和显式接口实现。

接口一直在使用,并且在现代的C#中仍然很流行。以下面的IDateProvider接口为例。

public interface IDateProvider
{
    DateTime GetDate();
}

没有什么特别的,现在设想两个实现 - 其中第一个隐式实现如下:

public class DefaultDateProvider : IDateProvider
{
    public DateTime GetDate()
    {
        return DateTime.Now;
    }
}

第二个显示实现是这样的:

public class MinDateProvider : IDateProvider
{
    DateTime IDateProvider.GetDate()
    {
        return DateTime.MinValue;
    }
}

注意显式实现如何省略访问修饰符。此外,方法名称被写为IDateProvider.GetDate(),,它将接口名称作为限定符的前缀。

上面两个例子使实现更加明确

显式接口实现的一个简洁之处是,它强制用户依赖于接口。显式实现接口的类的实例对象没有可用的接口成员 - 而是必须使用接口本身。

但是,当您将其声明为接口或将此实现作为预期接口的参数传递时,成员将按预期可用。

当它强制使用接口时,这一点特别有用。通过直接使用接口,您不会将代码耦合到底层实现。同样,显式接口实现处理命名或方法签名的模糊性 - 并使单个类可以实现具有相同成员的多个接口。

Jeffery Richter在他的书 CLR via C#中警告我们关于显式接口的现。两个主要的关注点是,当转换到显式实现的接口和方法时,值类型被装箱,而派生类型不能调用它们。

请记住,装箱和拆箱会带来额外能耗,和所有的编程一样,您应该评估测试用例以确定适合该工作的工具。

C#2.0版本

作为参考,我将列出C#2.0(ISO-2)的所有功能。

  • 匿名方法
  • 协变和逆变
  • 泛型
  • 迭代器
  • 可空类型
  • 局部类型

我最喜欢的功能是介于泛型和迭代器之间,我选择了泛型,下面我来说说原因。

对我来说这是一个非常困难的选择,我最终决定了泛型,因为我相信我比写迭代器更频繁地使用泛型。很多SOLID编程原则都是通过在C#中使用泛型来优化的,同样它也有助于保持代码的简洁。不要误解我的意思,我确实写了很多迭代器,这是一个值得在你的C#中采用的特性!

让我们更详细地看看泛型。

编者注: 学习如何使用 在C#中使用泛型来提高应用程序的可维护性。

泛型介绍: .NET Framework引入了类型参数的概念,这使得可以设计类和方法来推迟一个或多个类型的规范,直到类或方法被客户端代码声明和实例化为止。

让我们设想一下,我们有一个名为DataBag的类,可以作为一个数据包。它可能看起来像这样:

public class DataBag
{
    public void Add(object data)
    {
        // 为了简便起见,我们省略了...
    }
}

乍一看,这似乎是个很棒的主意,因为您可以在这个数据对象包的实例中添加任何东西。但当你真正思考这意味着什么时,这可能是相当令人担忧的。

所有添加的内容都隐式地转到了System.Object。此外,如果添加了值类型,则会发生装箱。这些是您应该注意的性能考虑事项。

泛型解决了这一切,同时也增加了类型安全性。让我们修改前面的例子,在类中包含一个类型参数T,并注意方法签名的变化。

public class DataBag
{
    public void Add(T data)
    {
       // 为了简便起见,我们省略了...
    }
}

现在,例如,DataBag实例只允许使用者添加DateTime实例。类型安全,没有类型强制转换或装箱的世界是美好的。

泛型类型参数也可以被限制。泛型约束是强大的,允许有限范围的可用类型参数,因为它们必须遵守相应的约束。有几种方法可以编写泛型类型参数约束,请参考以下语法:

public class DataBag where T : struct { /* T 是值类型*/ }
public class DataBag where T : class { /* T 可以是类接口等引用类型*/ }
public class DataBag where T : new() { /* T 必须有无参构造函数  */ }
public class DataBag where T : IPerson { /* T 继承IPerson */ }
public class DataBag where T : BaseClass { /* T 来源于BaseClass */ }
public class DataBag where T : U { /* T继承U, U也是泛型类型参数。 */ }

多个约束是允许的,我们只要用逗号分隔即可。类型参数约束立即被强制执行,这使得如果编译错误可以立即提醒我们。让我们看看下面DataBag类的约束条件。

public class DataBag where T : class
{
    public void Add(T value)
    {
        // 为了简便起见,我们省略了...
    }
}

现在,如果我试图实例化DataBag,C#编译器会让我知道我做错了什么。更具体地说,它指出:

类型'DateTime'必须是一个引用类型,以便将其用作泛型类型或方法'Program.DataBag'中的参数'T'

C#3.0版本

这里是C#3.0的主要功能列表。

  • 匿名类型
  • 自动实现属性
  • 表达树
  • 扩展方法
  • Lambda表达
  • 查询表达式

我在选择Lambda表达式的扩展方法的边缘蹒跚而行。但是,当我思考今天写的C#时,我实际上比其他任何C#运算符都更多地使用lambda运算符

我喜欢写富有表现力的C#。

在C#中有很多机会来利用lambda表达式和lambda运算符。使用=> lambda运算符将左边的输入与右边的lambda体分开。

一些开发人员喜欢将lambda表达式看作是表达委托调用的一种较为冗长的方式。Action,Func类型只是System命名空间中预先定义的泛型委托。

让我们从一个我们试图解决的问题开始,应用lambda表达式来帮助我们编写一些富有表现力和简洁的C#代码。

假设我们有大量的记录来代表天气趋势的信息。我们可能希望对该数据执行一些不同的操作,而不是在一个典型的循环中遍历它,因为我们可以以不同的方式处理这个问题。

public class WeatherData
{
    public DateTime TimeStampUtc { get; set; }
    public decimal Temperature { get; set; }
}

private IEnumerable GetWeatherByZipCode(string zipCode) { /* ... */ }

由于GetWeatherByZipCode的方法调用返回了一个IEnumerable,看起来您可能想要在循环中迭代这个集合。假设我们有一种计算平均温度的方法,它能做这项工作。

private static decimal CalculateAverageTemperature(
    IEnumerable<WeatherData>  weather,
    DateTime startUtc,
    DateTime endUtc)
{
    var sumTemp = 0m;
    var total = 0;
    foreach (var weatherData in weather)
    {
        if (weatherData.TimeStampUtc > startUtc &&
            weatherData.TimeStampUtc < endUtc)
        {
            ++ total;
            sumTemp += weatherData.Temperature;
        }
    }
    return sumTemp / total;
}

我们声明一些局部变量来存储在经过筛选的日期范围内的所有温度和它们的总和,然后计算平均值。在迭代中是一个逻辑if块,其检查天气数据是否在特定日期范围内。这可以改写如下:

private static decimal CalculateAverageTempatureLambda(
    IEnumerable<WeatherData>  weather,
    DateTime startUtc,
    DateTime endUtc)
{
    return weather.Where(w => w.TimeStampUtc > startUtc &&
                              w.TimeStampUtc  w.Temperature)
                  .Average();
}

如您所见,这大大简化了。逻辑if块实际上只是一个谓词,如果天气日期在范围内,我们将继续进行一些额外的处理——比如过滤器。然后我们把温度相加,所以我们只需要把这个项目选择出来。我们最终得到了一个经过筛选的温度列表,我们现在可以简单地调用平均值。

lambda表达式被用作在通用IEnumerable接口上的Where和选择扩展方法的参数。

C#4.0版本

从以前的版本发布来看,C#4.0的主要特性数量较少。

  • 动态绑定
  • 嵌入式互操作类型
  • 泛型协变和逆变
  • 实名/可选参数

所有这些功能都是非常有用的。但对我来说,它归结为实名和可选参数,而不泛型协变和逆变。在这两者之间,我讨论了我最常使用哪个特性,并且在多年的时间里,它确实使我受益最大。

我相信这个特性实名/可选的参数。这是一个非常简单的功能,但实用性得分很高。我的意思是,谁没有写一个重载或可选参数的方法?

当您编写可选参数时,您必须为其提供一个默认值。如果你的参数是一个值类型,那么它必须是一个字面值或者常数值,或者你可以使用default关键字。同样,您可以将值类型声明为Nullable,并将其赋值为null。让我们想象我们有一个Repository类,并有一个GetData方法。

public class Repository
{
    public DataTable GetData(
        string storedProcedure,
        DateTime start = default(DateTime),
        DateTime? end = null,
        int? rows = 50,
        int? offSet = null)
    {
        //为了简便起见,我们省略了...
    }
}

我们可以看到,这个方法的参数列表相当长,但是有几个任务。表示这些值是可选的。因此,调用者可以省略它们,并使用默认值。正如您可能假设的那样,我们可以仅通过提供存储过程名称来调用它。

var repo = new Repository();
var sales = repo.GetData("sp_GetHistoricalSales", rows: 100);

现在我们已经熟悉了可选参数特性以及这些特性如何工作,让我们在这里使用一些实名参数。以上面的示例为例,假设我们只希望我们的数据表返回100行而不是默认的50行。我们可以将我们的调用改为包含一个命名参数,并传递所需的重写值。

var repo = new Repository();
var sales = repo.GetData("sp_GetHistoricalSales", rows: 100);

C#5.0版本

像C#4.0版本一样,C#5.0版本中没有太多功能 - 但是其中一个功能非常庞大。

  • 异步/等待
  • CallerInfoAttributes

当C#5.0发布时,它实际上改变了C#开发人员编写异步代码的方式。虽然直到今天仍然有很多困惑,但我在这里向您保证,这比大多数人想象的要简单得多。这是C#的一个重大飞跃 - 它引入了一个语言级别的异步模型,它极大地赋予了开发人员编写外观和感觉同步(或者至少是连续的)的“异步”代码。

异步编程在处理I/O绑定工作负载(如与数据库,网络,文件系统等进行交互)时非常强大。异步编程通过使用非阻塞方法帮助处理吞吐量。这种方法使用了一个透明的异步状态机中的挂点和相应的延续。

同样,如果CPU负载计算的工作量很大,则可能需要考虑异步执行此项工作。这将有助于用户体验,因为UI线程不会被阻塞,而是可以自由地响应其他UI交互。

编者注:这里有一些关于C#异步编程的最佳实践,使用Async Await.

在C#5.0中,当语言添加了两个新的关键字async和await时,异步编程被简化了。这些关键字适用于Task。下表将作为参考:

Task表示异步操作。操作可以通过Task返回值,也可以通过Task返回void。当您使用async关键字修饰Task返回方法时,它使方法主体可以使用await关键字。当您请求await关键字的返回值时,控制流将返回给调用者,并且在方法的那个点执行暂停。当await的操作完成后,在同一点上恢复执行。部分代码如下!

class IOBoundAsyncExample
{

    private const string Url = "http://api.icndb.com/jokes/random?limitTo=[nerdy]";

    internal async Task GetJokeAsync()
    {
        using (var client = new HttpClient())
        {
            var response = await client.GetStringAsync(Url);
            var result = JsonConvert.DeserializeObject(response);

            return result.Value.Joke;
        }
    }
}
public class Result
{
    [JsonProperty("type")] public string Type { get; set; }
    [JsonProperty("value")] public Value Value { get; set; }
}

public class Value
{
    [JsonProperty("id")] public int Id { get; set; }
    [JsonProperty("joke")] public string Joke { get; set; }
}

我们用一个名为GetJokeAsync的方法定义一个简单的类。该方法是返回Task,这意味着我们的GetJokeAsync方法最终会给您一个字符串,或者可能出错。

该方法使用async关键字进行修饰,该关键字允许使用等待关键字。我们实例化并使用一个HttpClient对象。然后我们调用GetStringAsync函数,它接受一个字符串url并返回一个Task 。我们等待从GetStringAsync调用返回的Task。

当响应已经准备好时,就会继续发生并控制从我们曾经挂起的位置恢复。然后,我们将JSON反序列化到Result类的实例中,并返回Joke属性。

一些我最喜欢的成果

  • 查克·诺里斯(Chuck Norris)可以用单一的断言来测试整个应用程序。
  • 查克·诺里斯(Chuck Norris)可以编译语法错误。
  • 项目经理永远不会要求查克·诺里斯(Chuck Norris)做出估计。

欢闹随之而来!我们了解了C#5.0的惊人的异步编程模型。

C#6.0版本

C#6.0的推出有很多很大的进步,很难选择我最喜欢的功能。

  • 字典初始化
  • 异常过滤器
  • 在属性里使用Lambda表达式
  • nameof表达式
  • 空值运算符
  • 自动属性初始化
  • 静态导入
  • 字符串嵌入值

我把范围缩小到三个突出特点:空值运算符,字符串嵌入值和nameof表达式。

虽然nameof表达式很棒,我几乎每次都用它来编写代码,但其他两个特性更有影响力。这让我在字符串嵌入值和空值运算符之间做出决定,这是相当困难的。我决定我最喜欢的是字符串嵌入值,这就是为什么。

空值运算符是伟大的,它允许我写较少的详细代码,但它不一定能防止我的代码中的错误。但是,使用字符串嵌入值可以防止运行时错误 - 这是我的书中的一个胜利。

使用$符号启动字符串文字时,将启用C#中的字符串嵌入值语法。这指示C#编译器打算用各种C#变量,逻辑或表达式来插入此字符串。这是手动字符串连接甚至是string.Format方法的一个主要升级。考虑以下:

class Person
{
    public string FirstName { get; set; }
    public string LastName { get; set; }

    public override string ToString()
        => string.Format("{0} {1}", FirstName);
}

我们有一个简单的Person类,具有两个名称属性,用于名字和姓氏。我们重写ToString方法并使用string.Format。问题是,编译时,由于开发人员显然希望将姓氏也作为结果字符串的一部分,因此很容易出错,这一点在“{0} {1} ”参数中很明显。同样,开发人员可以很容易地交换名称或正确提供两个名称参数,但混乱的格式文字只包括第一个索引,等等...现在我们可以考虑使用字符串嵌入值。

class Person
{
    public string FirstName { get; set; } = "David";
    public string LastName { get; set; } = "Pine";
    public DateTime DateOfBirth { get; set; } = new DateTime(1984, 7, 7);

    public override string ToString()
        => $"{FirstName} {LastName} (Born {DateOfBirth:MMMM dd, yyyy})";
}

我冒昧添加DateOfBirth属性和一些默认的属性值。另外,我们现在在我们的ToString方法的覆盖中使用字符串嵌入值。作为一名开发人员,犯上述错误要困难得多。最后,我也可以在插值表达式中进行格式化。注意第三次嵌入值,DateOfBirth是一个DateTime - 因此我们可以使用您已经习惯的所有标准格式。只需使用:运算符来分隔变量和格式。

示例输出

· David Pine (Born July 7, 1984)

编辑注:有关C#6.0新特性的详细内容,请阅读www.dotnetcurry.com/csharp/1042/csharp-6-new-features

C#7.0版本

从所有集成到 C# 7.0的特性中。

  • 更多的函数成员的表达式体
  • 局部函数
  • Out变量
  • 模式匹配
  • 局部变量和引用返回
  • 元组和解构

我结束了模式匹配,元组和Out变量之间的争论。我最终选择了Out变量,这是为什么。

模式匹配但是我真的不经常使用它,至少现在还没有。也许以后我会更多地使用它,但是对于我迄今为止编写的所有c#代码,没有太多地方可以利用它。同样,这是一个很棒的功能,我确实看到了它的位置 - 只是在C#7.0中这不是我最喜欢的。

元组也是一个很好的补充。元组是语言的重要组成部分,成为一流的公民是非常棒的。我会说,“写tem1,.Item2,.Item3等...的日子已经过去了,但这并不一定是正确的。反序列化失去了元组的名称,使得这个公共API不那么有价值

我也不喜欢ValueTuple类型是可变的这一事实。我只是不明白设计者的决定。我希望有人能给我解释一下,但感觉有点像疏忽。因此,我得到了选择out变量的特性。

自从C#版本1.0以来,try-parse模式已经在各种值类型中出现了。模式如下:

public boolean TryParse(string value, out DateTime date)
{
    // 为了简便起见,我们省略了.....
}

该函数返回一个布尔值,指示给定的字符串值是否能够被解析。如果为true,则将分析的值分配给生成的输出参数date。它的使用如下:

DateTime date;
if (DateTime.TryParse(someDateString, out date))
{
    //  date现在是解析值
}
else
{
    // date是DateTime.MinValue,默认值
}

这种模式是有用的,但有点麻烦。有时,不管解析是否成功,开发人员都会采取相同的操作过程。有时使用默认值是可以的。C#7.0中的out变量使得这个更复杂,不过在我看来不那么复杂。

示例如下:

if (DateTime.TryParse(someDateString, out var date))
{
    // date现在是解析值
}
else
{
    // date是DateTime.MinValue,默认值
}

现在我们移除了if语句块的外部声明,并把声明作为参数本身的一部分。使用var是合法的,因为类型是已知的。最后,date变量的范围没有改变。它从内联声明泄漏到if块的顶部。

你可能会问自己:“为什么这是他最喜欢的功能之一?”.....这种感觉真的没有什么变化。

但是这改变了一切!

它使我们的C#更具有表现力。每个人都喜欢扩展方法,对 - 请考虑以下几点:

public static class StringExtensions
{
    private delegate bool TryParseDelegate(string s, out T result);

    private static T To(string value, TryParseDelegate parse)
        => parse(value, out T result) ? result : default;

    public static int ToInt32(this string value)
        => To(value, int.TryParse);

    public static DateTime ToDateTime(this string value)
        => To(value, DateTime.TryParse);

    public static IPAddress ToIPAddress(this string value)
        => To(value, IPAddress.TryParse);

    public static TimeSpan ToTimeSpan(this string value)
        => To(value, TimeSpan.TryParse);
}

这个扩展方法类很简洁,表达能力强。在定义了遵循try-parse模式的私有委托之后,我们可以编写一个泛型复合函数,它需要一个泛型类型的参数、要解析的字符串值和TryParseDelegate。现在我们可以安全地依赖这些扩展方法,考虑以下几点::

public class Program
{
    public static void Main(string[] args)
    {
        var str =
            string.Join(
                "",
                new[] { "James", "Bond", " +7 " }.Select(s => s.ToInt32()));

        Console.WriteLine(str); // 打印 "007"
    }
}

编辑注:要了解C#7的所有新功能,请查看本教程www.dotnetcurry.com/csharp/1286/csharp-7-new-expected-features

结论

这篇文章对我个人而言颇具挑战性。我喜欢C#的许多特性,因此每次发布只收集一个最喜欢的内容是非常困难的。

每个较新版本的C#都包含了强大而有影响力的功能。C#语言团队以无数的方式进行创新 - 其中之一就是引入点发布。在撰写本文时,C# 7.1和 7.2已正式发货。作为C#开发人员,我们生活在一个激动人心的语言时代!

然而,对我来说,对所有这些特性进行分类是相当有见地的;因为它帮助我们了解了什么是实际的,最影响我的日常发展。一如既往,努力成为一个务实的开发者!并不是语言中所有可用的特性都是当前任务所必需的,但了解什么是可用的,这一点很重要。

当我们期待C#8的建议和原型时,我对C#的未来感到兴奋。它看起来确实很有希望,而且语言正在积极地试图缓解“价值亿万美金的错误”。

欢迎转载,转载请注明翻译原文出处(本文章),原文出处(原博客地址),然后谢谢观看

如果觉得我的翻译对您有帮助,请点击推荐支持:)

C#历年来最受欢迎功能的更多相关文章

  1. C#最受欢迎功能 -- C#1至C#7

    不定时更新翻译系列,此系列更新毫无时间规律,文笔菜翻译菜求各位看官老爷们轻喷,如觉得我翻译有问题请挪步原博客地址 本博文翻译自: http://www.dotnetcurry.com/csharp/1 ...

  2. 常见的web测试功能点测试思路

    常见的功能点的测试思路: . 新增 或 创建(Add or Create) ) 操作后的页面指向 )操作后所有绑定此数据源的控件数据更新,常见的排列顺序为栈Stack类型,后进先出 ) 取消操作是否成 ...

  3. .NETCore 新型 ORM 功能介绍

    简介 FreeSql 是一个功能强大的 .NETStandard 库,用于对象关系映射程序(O/RM),支持 .NETCore 2.1+ 或 .NETFramework 4.6.1+. 定义 IFre ...

  4. 痞子衡嵌入式:利用i.MXRT1060,1010上新增的FlexSPI地址重映射(Remap)功能可安全OTA

    大家好,我是痞子衡,是正经搞技术的痞子.今天痞子衡给大家介绍的是i.MXRT部分型号上新增的FlexSPI Remap功能. OTA升级设计几乎是每个量产客户都绕不开的话题,产品发布后免不了要做固件( ...

  5. BPM流程中心解决方案分享

    一.需求分析 在过去办公自动化的浪潮中,很多企业已经实施了OA流程,但随着客户的发展和对流程管理的越来越重视, 客户对流程应用需求越来越深 入,您可能面临以下需求: 1.流程功能不能满足需求,包括流程 ...

  6. .NET环境下基于RBAC的访问控制

    .NET环境下基于RBAC的访问控制 Access Control of Application Based on RBAC model in .NET Environment 摘 要:本文从目前信息 ...

  7. 进一步丰富和简化表单管理的组件:form.js

    上文<简洁易用的表单数据设置和收集管理组件>介绍了我自己的表单管理的核心内容,本文在上文的基础上继续介绍自己关于表单初始值获取和设置以及表单数据提交等内容方面的做法,上文的组件粒度很小,都 ...

  8. java多线程(精华版)

    在 Java 程序中使用多线程要比在 C 或 C++ 中容易得多,这是因为 Java 编程语言提供了语言级的支持.本文通过简单的编程示例来说明 Java 程序中的多线程是多么直观.读完本文以后,用户应 ...

  9. 马旭飞:共探H3 BPM社区发展战略

    近日,以"让天下没有难用的流程"为主题,H3 BPM10.0在北京金隅喜来登酒店正式发布. H3 BPM全新的业务流程管理系统是颠覆BPM行业的巨作,拥有众多独创技术,近200个业 ...

随机推荐

  1. vue.js之过滤器,自定义指令,自定义键盘信息以及监听数据变化

    一.监听数据变化 1.监听数据变化有两种,深度和浅度,形式如下: vm.$watch(name,fnCb); //浅度 vm.$watch(name,fnCb,{deep:true}); //深度监视 ...

  2. Oracle学习笔记_10_判断是否为日期类型

    FUNCTION isdate (datestr VARCHAR2, format VARCHAR2) RETURN number IS p_date DATE; BEGIN SELECT TO_DA ...

  3. linux mysql添加、删除用户、用户权限及mysql最大字段数量

    1.  登录: mysql -u username -p 显示全部的数据库: show databases; 使用某一个数据库: use databasename; 显示一个数据库的全部表: show ...

  4. mysql 数据表字段修改sql 语句

    1 新增字段 alter table bulletin add citycode varchar(6) not null default 0 [after `id`]; # 城市代码 2 修改字段 a ...

  5. Asp.net MVC在Razor中输出Html的两种方式

    http://qubernet.blog.163.com/blog/static/177947284201485104616368/ Razor中所有的Html都会自动编码,这样就不需要我们手动去编码 ...

  6. JAVA NIO学习四:Path&Paths&Files 学习

    今天我们将学习NIO 的最后一章,前面大部分涉及IO 和 NIO 的知识都已经讲过了,那么本章将要讲解的是关于Path 以及Paths 和 Files 相关的知识点,以对前面知识点的补充,好了言归正传 ...

  7. JS中将一个值转换为字符串的3种方法

    1.value.toString() 2."" + value 3.String(value) 第一种方法存在的问题是,它不能把null和undefined转换为字符串.还有第二种 ...

  8. Python正则表达式返回首次匹配到的字符及查询的健壮性

    re.findall(pattern,string)会搜索所有匹配的字符,返回的是一个列表,获取首个匹配需要re.findall(pattern,string)[0]访问, 但是如果findall没匹 ...

  9. python爬虫入门-开发环境与小例子

    python爬虫入门 开发环境 ubuntu 16.04 sublime pycharm requests库 requests库安装: sudo pip install requests 第一个例子 ...

  10. .net4.5中HttpClient使用注意点

    .net4.5中的HttpClinet是个非常强大的类,但是在最近实际项目运用中发现了些很有意思的事情. 起初我是这样用的: using (var client = new HttpClient()) ...