下发布可执行文件_[Devblog文章翻译].NET Core 3.0已正式发布(全文版)

e80546bcea9d8e6000d0099e33572957.png

翻译自Announcing .NET Core 3.0

3.0实在很精彩,这篇文章也实在很长。

我也实在才略有限。 出错或许很多,希望看官随手指出。我自己也会慢慢润色。 有些领域的术语我也不是很熟,有些地方我就保留原文了,以及native这种耳熟能详的概念我也没处处翻译。

不喜欢看长文的朋友也可以选择看这篇总结版的专栏文章:

森林蝙蝠:.NET Core 3正式发布​zhuanlan.zhihu.com
edaf5ca7343838f5eb0dd235aea69795.png

文章主要讲了有关C#8方面的改进、发布时.NET Core程序打包成可执行文件以及编译相关的东西、docker支持方面的改进,大家可以选择性阅读。

最重要的是快去更新3.0吧!

正文

我们激动地宣布.NET Core 3.0正式发布。3.0包含了大量的改进,3.0中增加了对Windows Forms和WPF的支持、有了新的JSON API、添加了对ARM64的支持,性能也有了全面的提升。本次发布中也包含了C# 8,在C# 8中添加了可空类型、异步流(async stream)以及更多的模式。一同发布的还有F# 4.7,着重改进了语法并且支持以.NET Standard 2.0为目标。从今天(9月23日)开始就可以将现存的项目的生成目标更新至3.0。本次发布兼容之前的版本,可以非常简单地更新。

在这里下载WIndows、macOS、Linux版本的.NET Core 3.0:

.NET Core

Snap

Docker

ASP.NET Core 3.0 和EF Core 3.0也于同一天发布。

关于3.0你应该知道的

在深入了解3.0中的新特性之前,应当注意这些关键性的改进和指南。

  • .NET Core 3.0 已经经过了实战测试。已经在http://dot.net 和 http://bing.com 上托管了数月。微软的其他团队还将在生产中继续在.NET Core 3.0上部署高额负载。
  • 性能得到了极大的提升。详见Performance Improvements in .NET Core 3.0 | .NET Blog
  • C# 8增加了异步流、range/index、更多的模式、可空引用类型。可空类型让你可以直面代码中会导致NullReferenceException的错误。框架库的最底层已经进行了标记,让你能够知道什么时候可能会得到null。
  • F#着重让隐式yield更加简单,以及语法上的改进。还添加了对LangVersion的支持,同时还有nameof,并在预览版中增加了对静态类的支持。 FSharp Core库现在也以.NET Standard 2.0为目标。详见
  • .NET Standard 2.1增加了可以同时在.NET Core和Xamarin中使用的类型。2.1包含了从.NET Core 2.1开始存在的类型。
  • Windows桌面应用程序,包括WIndows Forms和WPF,现在都受.NET Core支持。WPF设计器是VS2019 16.3的一部分,而WIndows Forms的设计器仍在预览阶段中,现在可以以扩展的形式下载。
  • .NET Core现在在默认情况下拥有可执行文件。在之前的版本中,应用程序必须通过dotnet命令启动,比如说dotnet myapp.dll。现在应用程序可以通过每个app单独的可执行程序启动,比如在不同的操作系统上使用myapp或者./myapp启动。
  • 高性能JSON API现已添加到3.0中,适用于读写、对象模型、序列化等各种场景。这些API建立在Span之上,并且使用的是UTF8而不是UTF16(比如说string),极大地减少了内存分配工作,因而获得了更高的性能、更少的GC消耗。详见.NET 3.0中JSON的未来
  • 内存使用比以前少得多的垃圾回收器。这项改进对于在同一个服务器上部署很多应用程序的场景下非常有用。垃圾回收器现在可以更好地利用超多核心(64核+)。
  • .NET Core已为Docker增强。现在.NET应用程序可以更加稳定高效地运行在Docker容器中。垃圾回收器和线程池现在可以更好地在内存和CPU受限的容器中运行。.NET Core的Docker镜像现在变得更小了,尤其是SDK镜像。
  • 树莓派和ARM芯片现在已受.NET Core支持以进行物联网开发,还包括VS远程调试器的支持。你可以使用全新的GPIO API部署监听传感器、在显示器上打印文字或图片的应用程序。http://ASP.NET现在可以当作API来暴露数据,或者作为一个网站来配置物联网设备。
  • .NET Core 3.0是一个“当前最新版本”的版本。下一个版本是预计在2019年十一月发布的.NET Core 3.1。3.1会是下一个LTS版本(意思是至少三年内受支持)。我们推荐您目前使用3.0,之后采用3.1。更新会非常轻松。
  • .NET Core 2.2将在12/23结束支持。它是上一个“当前最新版本”。
  • .NET Core 3.0将支持RHEL 8。
  • VS2019 16.3是必须更新的——如果你想在使用VS进行.NET Core 3.0开发。
  • VS for Mac8.3是必须更新的——如果你想在VS for Mac上进行.NET Core 3.0开发
  • VS Code用户只需使用最新的C#扩展来确保支持最新的框架版本,包括.NET Core 3.0。
  • Azure App Service对3.0对部署支持目前正在进行。
  • Azure Dev Ops对3.0的部署支持即将到来。届时会进行更新。

支持平台

支持下列操作系统:

  • Alpine: 3.9+
  • Debian: 9+
  • openSUSE: 42.3+
  • Fedora: 26+
  • Ubuntu: 16.04+
  • RHEL: 6+
  • SLES: 12+
  • macOS: 10.13+
  • Windows Client: 7, 8.1, 10 (1607+)
  • Windows Server: 2012 R2 SP1+

注意:Windows Forms和WPF仅支持Windows。

支持下列芯片:

  • x64 on Windows, macOS, and Linux
  • x86 on Windows
  • ARM32 on Windows and Linux
  • ARM64 on Linux (kernel 4.14+)

注意:请确保在ARM64设备上的部署使用4.14版本以上的Linux内核。比如Ubuntu18.04满足要求,但16.04不行。

WPF和Windows Forms

现在你可以在Windows上使用.NET Core 3开发WPF和Windows Forms应用程序。为了让桌面应用程序能够轻松地从.NET Framework迁移到.NET Core,在项目开始时我们的目标就是能够提供极好的兼容性。我们从许多已经成功将他们的应用程序迁移到.NET Core 3.0的开发者那里收到了许多反馈。在很大程度上,我们让WPF和Windows Forms在保持原样的情况下,能够在.NET Core上运行。尽管实际项目并不是这样,但是可以像这样想。

下图展示了一个.NET Core WIndows Forms应用程序:

299b441347b7034b11dc16dadb12ab15.png

VS2019 16.3支持创建以.NET Core为目标的WPF应用程序。包括新的模版和新的XAML设计器(设计器是以.NET Framework为目标的),不过你会注意到体验上有一些变化。有一个在技术上的巨大差异就是,.NET Core的设计器使用了一个新的界面进程(wpfsurface.exe),它会独立地运行以.NET Core为目标的代码。在这之前,.NET Framework设计器进程(xdesproc.exe)本身就是一个WPF .NET Framework进程,设计器就寄宿在它上面。由于运行时的兼容性问题,我们不能让一个WPF .NET Framework进程(在本例中就是VS)加载两个版本的.NET(.NET Framework和.NET Core)到同一个进程中。这就意味着设计器的某些地方不能像以前那样工作,比如说设计器扩展。如果你在编写设计器扩展,我们推荐您阅读XAML 设计器扩展性迁移 - Visual Studio | Microsoft Docs

下图展示了WPF应用程序显示在新的设计器中的样子: 

cf46ab090e15336147edb0586c5ac8ab.png

Windows Forms的设计器目前仍在预览阶段,可以在这里单独下载。 在VS之后的版本中会被加入。设计器目前包含了对大多数常用控件和低级功能的支持。在每月的更新中我们会持续更新。我们现目前不建议将您的Windows Forms项目迁移到.NET Core,特别是您依赖于设计器的情况下。不过请一定要试试预览版的设计器并给予我们反馈。

你也可以使用.NET CLI从命令行界面中创建并生成桌面应用程序。

比如您可以像下面这样快速地创建新的Windows Forms应用程序:

dotnet new winforms -o myapp
cd myapp
dotnet run

你可以用同样的流程创建WPF应用程序:

dotnet new wpf -o mywpfapp
cd mywpfapp
dotnet run

我们在2018年已经让Windows Forms和WPF开源。能看到社区和Windows Forms以及WPF团队一起改进这些UI框架实在是很棒。对于WPF来说,我们一开始在GitHub的仓库中只开源了一小部分代码。而目前几乎WPF的所有代码都已经发布到了GitHub上,未来还将陆续更新更多的组件。而其他.NET Core项目,这些新的仓库是基于MIT许可证开源的,并作为.NET基金会的一部分。

System.Windows.Forms.DataVisualization这个包(包含了图表控件)也支持.NET Core。您现在可以在您的.NET Core应用程序中加入这个控件。图表控件的源代码也公开到了GitHub上的这个仓库中。该控件已经为了便于迁移到.NET Core 3更新,但是我们将来不会对它进行重大更新。

Windows Native互操作

Windows以C API、COM、WinRT的形式提供了大量native API。从.NET Core 1.0起我们就有P/Invoke的支持,并且我们在3.0中加入了CoCreate COM API的能力和将托管代码暴露为COM组件的能力。我们收到了许多关于这些功能的请求,这些功能一定会大有用处。

去年年末我们宣布我们成功地实现了在.NET Core上进行Excel自动化。实在是很有趣。实际上,这个demo使用了像NOPIA、对象相等性、自定义marshaller等COM互操作特性。您可以在扩展示例中试试这个demo以及其他的demo。

在.NET Core 3.0中对托管C++和WinRT有部分支持,在3.1中会完整支持。

可空引用类型

C# 8.0引入了可空引用类型和不可空引用类型。这些特性让你对引用类型的属性进行重要的陈述: 这个引用不应当为null。当变量不应当为null时,编译器会强制使用一些规则来确保在不事先进行null检查的情况下,可以安全地获取引用的内容(dereference)。 这个引用可以是null。当变量可以是null时,编译器会加上不同的规则来去啊宝你正确地进行了null检查。

在之前版本的C#中,从变量的生命中无法推断出设计意图,而这个特性在这个问题上提供了巨大的好处。利用可空引用类型,你可以更加清楚地表明你的意如,而编译器也会帮助你正确地完成这项工作并且发现代码中的bug。

参阅这篇文章和这篇(This is how you get rid of null reference exceptions forever | On .NET | Channel 9)了解更多。

接口成员的默认实现

在当前,如果你发布了一个接口,你要修改它那就天下大乱了————如果你不破坏所有现有的实现,那么你就不能添加新的接口成员。

在C# 8.0中,你可以为接口成员提供默认实现。因此如果一个类实现了某个接口但并没有实现某个成员(可能在当初写代码时还没有这个成员),那么调用的代码就会转而使用默认实现。

interface ILogger
{
    void Log(LogLevel level, string message);
    void Log(Exception ex) => Log(LogLevel.Error, ex.ToString()); // New overload
}
class ConsoleLogger : ILogger
{
    public void Log(LogLevel level, string message) { … }
    // Log(Exception) gets default implementation
}

在上面的例子中,ConsoleLogger对ILogger的Log(Exception)方法并没有实现。因为它有它的默认实现。现在你可以为既存的接口添加新的成员,只要你为已经实现了接口的类型提供了可用的默认实现。

异步流

现在您可以通过IAsyncEnumerable<T>来用foreach迭代异步数据流。这就是你所期待的新的接口————异步版本的IEnumerable<T>。C#让你可以用async foreach来迭代各个task进而消费(consume)他们的各个元素。在生产方(production side),你可以用yield return各个项目来产生异步流。听起来可能有点复杂,但是实施起来十分简单。

下面的例子模拟了异步流的产生和消耗。foreach语句是async的,并且它自己使用yield return来为调用者产生异步流。我们推荐使用这种使用yield return来产生异步流的模式。

async IAsyncEnumerable<int> GetBigResultsAsync()
{
    await foreach (var result in GetResultsAsync())
    {
        if (result > 20) yield return result;
    }
}

除了可以await foreach,你还可以创建async的迭代器。比如说返回IAsyncEnumerable/IAsyncEnumerator的迭代器,你可以使用await和yield return。对于需要释放资源的对象,你可以用IAsyncDisposable。像Stream和Timer之类的框架库中的类型都实现了这个接口。

Index和Range

我们为数组元素的访问和其他任何直接暴露数据访问功能的类型创造了全新的语法和类型,你可以用它们来描述索引器(indexer)。该特性包含了对单一值(索引通常只有一个值),和两个值(描述一个范围)的支持。

Index是一个描述数组索引的新类型。你可以用一个int值来描述起始位置的Index,或者在前面加一个^操作符来从后往前的Index。在下面的例子中你会看到这两种用法:

Index i1 = 3;  // number 3 from beginning
Index i2 = ^4; // number 4 from end
int[] a = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 };
Console.WriteLine($”{a[i1]}, {a[i2]}”); // “3, 6”

类似的,Range包含两个Index值,一个是起始位置,另一个是结束位置。可以写成x..y这样的范围表达式(译者:惊了,C#也终于有这个了)。随后你就可以通过Range来进行数据切片,就像下面的例子这样:

var slice = a[i1..i2]; // { 3, 4, 5 }

Using声明

使用using语句时被缩进整烦了?现在不用了!你现在可以像下面这样写,在当前代码块的作用域中加上一句using声明,在代码块结束的地方就自动释放。(译者:我倒是觉得原来那样能够更直观地看到我哪里使用了这样的对象。。。)

using System;
using System.Linq;
using System.Collections.Generic;
using static System.Console;
using System.IO;

namespace usingapp
{
    class Program
    {
        static void Main()
        {
            var filename = “Program.cs”;
            var line = string.Empty;
            var magicString = “magicString”;

            var file = new FileInfo(filename);
            using var reader = file.OpenText();
            while ((line = reader.ReadLine())!= null)
            {
                if (line.Contains(magicString))  
                { 
                    WriteLine(“Found string”); 
                    return;
                }
            }

            WriteLine(“String not found”);
        } // reader disposed here
    }
}

Switch表达式

使用C#的人可能都喜欢switch语句的想法,但并不喜欢switch的语法。 8.0中引入了switch表达式,它让switch:

  • 拥有简洁的语法
  • 由于它是表达式因此可以返回值
  • 和模式匹配结合

switch关键字是“中置”(infix)的,意味着关键字放在被测值(在第一个例子中就是o)和case列表的中间。和lambda表达式很像。

第一个例子使用了lambda语法来定义方法,它和switch表达式可以很好地结合起来。不过并不是一定要这样做。

static string Display(object o) => o switch
{
    Point { X: 0, Y: 0 }         => “origin”,
    Point { X: var x, Y: var y } => $”({x}, {y})”,
    _                            => “unknown”
};

在上面的例子中有两个模式。o首先和Point类型的模式进行匹配,随后和大括号里面的属性模式进行匹配。_描述了丢弃的模式,和switch语句中的default是一样的。

您还可以更进一步,依靠元组析构和参数位置。就像下面这样:

static State ChangeState(State current, Transition transition, bool hasKey) =>
    (current, transition) switch
    {
        (Opened, Close)              => Closed,
        (Closed, Open)               => Opened,
        (Closed, Lock)   when hasKey => Locked,
        (Locked, Unlock) when hasKey => Closed,
        _ => throw new InvalidOperationException($”Invalid transition”)
    };

上面的例子中,您会发现毋需为每个case定义一个变量或者显式的类型。编译器会把作为测试值的元组和为每个case定义的元组进行匹配。

这些模式让您能够编写可以抓住您的意图的代码,而不是对其进行测试的顺序式代码。编译器会负责实现这些枯燥无味的顺序式代码,并且会保证正确性。

引入更快的JSON API

.NET Core 3.0加入了新的JSON API。它支持读写场景、DOM随机访问、序列化器。您可能很熟悉Json.NET,而新的API意在能够在许多相同的场景使用,并减少内存消耗和更快的执行速度。

你可以在The future of JSON in .NET Core 3.0中看到该计划的描述和最初的动机。这篇文章由Json.NET的作者撰写,解释了为什么要创造这样一个新的API而不是对现有的Json.NET进行扩展。简言之,我们想充分利用新.NET Core的性能来创造一个新的JSON API。而在照顾兼容性的同时使用现有Json.NET代码库是做不到这一点的。

Utf8JsonReader

System.Text.Json.Utf8JsonReader是一个高性能、低内存消耗、只进的reader。它从ReadOnlySpan<byte>中读取UTF-8编码的JSON文本。Utf8JsonReader是一个基本的、低级的类型。可以利用它来创建自定义转换器和反序列化器。使用新的Utf8JsonReader读取JSON负载比Json.NET快了两倍。只要在您需要将JSON的token实现为UTF16字符串时,它才会要求分配更多的内存。

Utf8JsonWriter

System.Text.Json.Utf8JsonWriter提供高性能的、非缓存的、只进的方式来以UTF-8编码写入JSON文本。它可以写入想String、Int32、DateTime等各种常见的.NET类型。和Reader一样,Writer也是基本的、低级的类型,可以用于创建自定义序列化器。使用新的Utf8JsonWriter比Json.NET快了30-80%,并且不消耗额外内存。

JsonDocument

System.Text.Json.JsonDocument提供了转换JSON数据和创建只读DOM的能力。这些DOM可以通过查询来支持随机访问和枚举。它基于Utf8JsonReader创建。通过 JsonDocument暴露的JsonDocument类型的RootElement属性,可以访问组成数据等JSON元素。JsonElement包含了JSON数组和枚举器,以及可以将JSON文本转换问常见.NET类型的API。 使用JsonDocument转换典型的JSON负载和访问其所有的元素比在Json.NET中快了2-3倍,并且只进行了非常少的内存消耗(小于1MB)。

JSON序列化器

System.Text.Json.JsonSerializer建立在高性能的Utf8JsonReaderUtf8JsonWriter之上。它从JSON反序列化对象,将对象序列化为JSON。能够保持极小的内存消耗并且支持使用Stream异步读写JSON。

示例和其他信息详见文档。

引入新的SqlClient

SqlClient是一个数据提供者(data provider)。它让你能够访问Microsoft SQL Server和Azure SQL Database,或者像EF Core和Dapper这样流行的.NET O/RM,又或是直接使用http://ADO.NET API。现在它已经更新并作为Microsoft.Data.SqlClient包发布。支持.NET Framework和.NET Core应用程序。通过NuGet的使用,SQL团队为.NET Framework 和NET Core用户提供更新会变得更加简单。

(下面是大家并不这么感兴趣的环节)

ARM和物联网支持

继2.1、2.2中加入了对Linux和Windows上ARM32的支持之后,在本次版本发布中,我们添加了对Linux ARM64的支持。一些物联网工作负载已经得到了既存的x64支持,但许多用户一直想要对ARM的支持。现在有了,并且我们正在和计划大规模部署的客户进行合作。

许多使用.NET部署的物联网都是边缘设备,并且完全面向网络。其他的场景都必须直接访问硬件。在本次发布中,我门添加了能够使用Linux串口和利用设备(比如说树莓派)的数字引脚的能力。数字引脚会使用各种不同的协议。我们加入了对GPIO、PWM、I2C、SPI的支持,以便能够读取传感器数据、和无线电互动、在显示器上写入文本和图片等等各种场景。

上述功能是下列包的一部分:

  • System.Device.Gpio
  • Iot.Device.BIndings

作为提供GPIO支持的提供者(也作为朋友),我们看了一下已经可用的特性。我们找到了为C#和python提供的API。对于两者来说,这些API都是对native库的封装,通常基于GPL协议。我们不想继续在这条路上走下去,相反我们通过100%的CSharp构建了实现这些协议的解决方案。意味着我们的API可以在任何支持.NET Core的地方工作,也可以使用CSharp调试器调试(通过sourcelink),并且支持多种Linux驱动器(sysfs、libgpiod、board-specific)。所有的代码都基于MIT协议。我们认为和既存的方式相比,者对于.NET开发者来说是一个巨大的进步。

在这里了解更多。

.NET Core运行时roll-forward规则更新

.NET Core运行时————实际上是运行时binder,现在可以可选择主版本roll-forward作为规则。运行时bidner已经默认可以选择在补丁和副版本上roll-forward。我们认为在一些场景这些规则很重要,因此暴露了更多的规则,但是我们没有改变默认的roll-forward规则。

有一个叫做RollForward的新属性,它接受下列值:

  • LatestPatch————向前roll到最高的补丁版本。该项禁用了Minor规则。
  • Minor————如果找不到请求到副版本,向前roll到最低到副版本。如果有请求到副版本,那么就使用LatestPatch规则。这是默认的规则。
  • Major————如果找不到请求的主版本,向前roll到最低的主版本、副版本。如果有请求的主版本,那么使用Minor规则。
  • LatestMinor————向前roll到最高的副版本,即使请求的副版本存在。
  • LatestMajor————向前roll到最高的主版本和副版本,即使请求的主版本存在。
  • Disable————不向前roll。只绑定到指定版本。常规使用不推荐该规则,因为它禁用了roll-forward到最新的补丁。只推荐测试时使用。

在Runtime Binding Behavior和dotnet/core-setup 5691查看更多信息。

Docker和cgroup的限制

许多开发者在容器中打包和运行他们的应用程序。一个很重要的情景就是容器的资源受限,比如CPU或者内存。我们在2017年实现了对受限内存的支持。不过不幸的是我们发现对于在受限的配置下保持稳定性我们的实现还不够有效,当设置了内存限制(特别是小于500MB)时应用程序还是会被OOM杀掉。在3.0中我们修复了这个问题。我们强烈推荐.NET Core Docker用户更新到3.0来获得这方面的改进。 Docker资源限制建立在cgroup上,这是一个Linux内核特性。在运行时的角度来说,我们需要着重处理cgroup primitive。

你可以使用docker run -m参数来限制容器的可用内存,就像下面这样创建了基于Alpine的容器,它只有4MB内存(然后打印了内存限制)

C:>docker run -m 4mb —rm alpine cat /sys/fs/cgroup/memory/memory.limit_in_bytes
4194304

为了更好地支持CPU限制(—cpus)我们还做了一些改进。包括改变运行时对CPU decimal值取整的方式。—cpus参数让一个值(足够)接近一个更小的整数(比如1.499999999999),在之前运行时会把这个数向下取整(前面的例子中就是1)。最终运行使用了比请求的CPU更少的资源,让CPU不能充分利用。而通过向上取整,运行时增大了OS线程scheduler的压力,但是即便是最坏的情况下(—cpus=1000000001——之前取整为1,现在是2),我们也没有发现任何导致CPU性能降低的过度使用。

下一步是确保线程池能够遵循CPU限制。线程池的算法的一部分会计算CPU的繁忙时间(busy time),这部分在计算可用CPU的函数之中。通过在计算CPU繁忙时间时,考虑CPU的限制,我们就避免了引起线程池之间的相互竞争:一个想分配更多线程来增加CPU的繁忙时间,而另一个因为添加更多的线程并不能提升吞吐量又想分配更少的线程。

默认情况下GC堆更小

在提升对docker内存限制的支持这一部分,我们发觉要制定更加通用的GC规则来改进更多应用程序的内存使用方式(即使没有在容器中运行)。这些改动让第0代内存预算和现代处理器缓存大小和多级缓存对齐。

我们团队的Damian Edwards注意到ASP.NET基准测试中的内存使用降低了一半,并且在其他性能测试上没有副作用。一路走来着实不容易啊! 就像他说的那样,这些规则是新的默认设置,不需要额外修改他的(或者你的)代码(除了你需要使用3.0)。

我们在http://ASP.NET基准测试中看到的内存消耗降低可能也可能不会表现在你的应用程序中。我们希望从您那里了解到这些改进如何降低了您的应用程序的内存消耗。

对多处理器有更好的支持

由于基于.NET有关Windows的遗留物,GC必须实现Windows的处理器集群概念来支持64个以上数量的处理器。大概在5-10年前,在.NET Framework中得到了实现。在.NET Core中,我们最开始选择了Linux PAL来模拟同样的概念,尽管这样的概念在Linux上并不存在。因此从那以后我们就在GC中弃用了这个概念,只把他过渡到WIndows PAL。

GC现在暴露了一个配置开关:GCHeapAffinitizeRanges。用它在拥有64个处理器以上的机器上指定affinity mask。Maoni Stephens在这篇文章中描述了这项改进。

GC对大型页的支持

在大型页(large page)和巨型页(huge page)中,操作系统可以创建比原生页大小(通常是4K)更大的内存区域,从而可以提升请求大型页的应用程序的性能。

当发生从虚拟地址到物理地址到转换时,叫做转址旁路缓存(Translation Lookaside Buffer,TLB)的缓存会被优先(通常是并行的)查询。首先会检查某个虚拟地址物理地址是否已经被转换过,从而避免代价高昂的页表遍历。每一次大型页转换都使用了CPU中的单一转址缓存。这种缓存的大小通常比原生页大了三个数量级,使得转址缓存更加高效,进而提升了频繁访问的内存的性能。对于有两层TLB的虚拟机来说,这样的提升更为显著。

GC现在可以选择性地配置GCLargePages以便在Windows上分配大型页。使用大型页减少了了TLB脱靶的情况,从而潜在地提高了应用程序的总体性能。不过这个特性也有一些需要考虑到的限制。必应已经对这项特性进行了实验,并且得到了性能提升。

.NET Core版本API

在3.0中我们改进了.NET Core版本API。现在这些API能够返回您所期望的信息了。客观来讲这些改动让他们更好了,但是在技术上来说具有一定破坏性,并且有可能影响既存的依赖于版本API的应用程序。

现在您可以获取到如下的版本信息:

C:gittestappsversioninfo>dotnet run
**.NET Core info**
Environment.Version: 3.0.0
RuntimeInformation.FrameworkDescription: .NET Core 3.0.0
CoreCLR Build: 3.0.0
CoreCLR Hash: ac25be694a5385a6a1496db40de932df0689b742
CoreFX Build: 3.0.0
CoreFX Hash: 1bb52e6a3db7f3673a3825f3677b9f27b9af99aa

**Environment info**
Environment.OSVersion: Microsoft Windows NT 6.2.9200.0
RuntimeInformation.OSDescription: Microsoft Windows 10.0.18970
RuntimeInformation.OSArchitecture: X64
Environment.ProcessorCount: 8

事件管道的改进

事件管道现在支持多个会话。意味着你可以同时通过EventListener来in-proc地消耗事件,同时拥有out-of-process的事件管道客户端。

添加了新的性能计数器:

  • GC时间百分比
  • 第0代堆大小
  • 第1代堆大小
  • 第2代堆大小
  • LOH堆大小
  • 分配率
  • 已加载程序集数量
  • 线程池线程数量
  • 监控锁竞争比例
  • 线程池工作项队列
  • 线程池已完成工作项比例

分析器现在使用同样的事件管道基础设施实现。

参阅David Fowler所写的Play with counters来了解可以如何使用事件管道来进行性能测试或者监控应用程序状态。

在diagnostics/dotnet-counters-instructions.md at master · dotnet/diagnostics · GitHub安装dotnet计数器工具。

HTTP/2的支持

现在HttpClient支持HTTP/2。对于像是gRPC、Apple Push Notification Service这样的API来说,新版的协议是必须的。在未来我们预计会有更多需要HTTP/2的服务。ASP.NET也已经支持HTTP/2。

注意:HTTP协议偏好会通过TLS/ALPN协商,只有在服务器选择使用HTTP/2的时候才会被使用。

Tiered Compilation

层叠编译作为一个可选功能加入到了.NET Core 2.1中。这个功能让运行时可以更加适应性地使用JIT编译器,以便在启动时和最大化吞吐量时能够获得更好的性能。在3.0中将默认开启。在去年一年中,我们堆这项功能进行了大量的改进,包括在各种工作负载中测试,比如网站、PowerShell Core、WIndows桌面应用程序。现在性能已经好多了,因此我们让它默认开启。

IEEE浮点数的改进

浮点数API现已更新,遵循IEEE 754-2008 revision。.NET Core浮点数项目的目标就是暴露所有“必需”的操作,并确保其行为符合IEEE规范。

转换和格式化修正: 能够正确转换和取整任意长度的输入。 能够正确转换和格式化负0。 * 能够通过进行大小写敏感的检查并且允许可选的前置+号来正确转换Infinity和NaN。

新的数学API:

  • BitIncrement/BitDecrement————对应IEEE的nextUp和nextDown操作。分别返回比输入大或小的最小的浮点数。比如Math.BitIncrement(0.0)将返回double.Epsilon。
  • MaxMagnitude/MinMagnitude————对应IEEE的maxNumMag和minNumMag操作,分别返回两个输入中在数量级上更大或功效的数。比如Math.MaxMagnitude(2.0, -3.0)返回-3.0。
  • ILogB————对应IEEE的logB操作,返回一个整形值。它返回的是输入参数以2为底的对数。和floor(log2(x))等效,但是最大程度减少了取整错误。
  • ScaleB————对应IEEE的scaleB操作,要求一个整形值为参数,返回和x * pow(2, n)等效的结果,但是最大程度减少了了取整错误。
  • Log2————对应IEEElog2操作,返回以2为底的对数。最大程度减少了取整错误。
  • FusedMultiplyAdd————对应IEEEfma操作。进行乘法加法混合运算。即一次性完成(x * y) + z,因此最大程度减少了取整错误。比如FusedMultiplyAdd(1e308, 2.0, -1e308)会返回1e308。常规的(1e308 * 2.0) - 1e308会返回double.PositiveInfinity。
  • CopySign————对应IEEE的copySign操作。返回x的值,但是有y的符号。

.NET Platform Dependent Intrinsics

我们添加了允许访问特定面向性能的CPU指令集的API,比如说SIMD、Bit Manipulation指令集。这些指令可以帮助在特定场景实现极大的性能提升。比如说并行处理数据。除了暴露在您的程序中可以使用的API,我们也开始使用这些指令来加速.NET库。

下面的CoreCLR PR通过实现或者用例展示了一些内建指令的使用

  • Implement simple SSE2 hardware intrinsics
  • Implement the SSE hardware intrinsics
  • Arm64 Base HW Intrinsics
  • Use TZCNT and LZCNT for Locate{First|Last}Found{Byte|Char}

更多信息请参阅.NET Platform Dependent Intrinsics。其中定义了一种定义这种硬件基础设施的方法。这让微软、芯片供应商以及其他公司和个人可以定义能够暴露给.NET API的硬件/芯片API。

在Linux上支持OpenSSL 1.1.1和TLS 1.3

.NET Core现在利用了[OpenSSL 1.1.1中对TLS 1.3]的支持。 TLS 1.3对每个OpenSSL团队有很多好处:

  • 减少在客户端和服务器间必要的往返次数因而减少了连接所需时间。
  • 移除了多种过时且不安全的加密算法提高了安全性。

.NET Core 3.0可以利用OpenSSL 1.1.1,1.1.0,1.0.2。当1.1.1可用时,如果使用SslProtocols.None(系统默认协议),假设客户端和服务器都支持TLS 1.3,SslStream和HttpClient类型就会使用TLS 1.3。

十分期待.NET Core在Windows和macOS上也能支持TLS 1.3。

加密

通过System.Security.Cryptography.AesGcmSystem.Security.Cryptography.AesCcm的实现,我们加入了对AES-GCM、AES-CCM加密的支持。两种算法都是AEAD算法,并且说首先加入.NET Core的AE算法。

.NET Core 3.0现在支持从标准格式中导入和导出不对称公钥和私钥,切无需使用X.509证书。

所有密钥类型(RSA, DSA, ECDsa, ECDiffieHellman)都支持X.509SubjectPublicKeyInfo格式作为公钥、PKCS#8 PrivateKeyInfo 和 PKCS#8 EncryptedPrivateKeyInfo格式作为私钥。 RSA另外还支持PKCS#1 RSAPublicKey 和 PKCS#1 RSAPrivateKey。导出方法全都产生DER编码的二进制数据,导入方法也是一样。如果密钥保存在文本友好的PEM格式中,那么调用者就需要在调用导入方法之前对内容进行base-64编码。

PKCS#8文件可以通过System.Security.Cryptography.Pkcs.Pkcs8PrivateKeyInfo类检查。

PFX/PKCS#12文件可以通过System.Security.Cryptography.Pkcs.Pkcs12Info 和 System.Security.Cryptography.Pkcs.Pkcs12Builder分别进行检查和操作。

新的日本年号(令和)

2019年五月一日,日本启用新的年号令和。像.NET Core这样的支持日本历法的软件必需要更新以应对令和。.NET Core和.NET Framework已经更新并可以准确处理有新年号的日语日期格式化。

.NET依赖操作系统或其他更新来准确处理令和日期。如果你或者你的客户正在使用Windows,那么请下载Windows的最新更新。如果使用的是macOS或者Linux,请安装ICU version 64.2以支持新的日本年号。

参阅.NET博客上的这篇在.NET中处理日本历法中的新年号了解更多。

程序集加载上下文的改进

对AssemblyLoadContext的改进:

  • 启用命名上下文
  • 添加可以枚举ALC的能力
  • 添加可以枚举ALC中程序集的能力
  • 让类型具体化————因此实例化更快(对于简单场景不需要自定义类型)

详见这里。appwithalc这个例子展示了这些新功能。

通过使用AssemblyDependencyResolver和自定义的AssemblyLoadContext,一个应用程序可以加载插件,并且可以让插件的依赖从正确的地方加载,插件的依赖也不会相互冲突。AppWithPlugin示例包含了依赖有冲突的插件和依赖其他程序集和native库的插件。

程序集可卸载

程序集的卸载是AssemblyLoadContext的性能。这项功能对于API来说很大程度上是透明的,它仅仅暴露了一些新API。它启用了一个可以被卸载的加载器上新闻,还会释放所有已实例化类型、静态字段、程序集自身的内存。一个应用程序应当可以随时在没有内存泄漏的情况下通过这种机制加载和卸载程序集。

我们认为可以在下列场景中使用这种新功能:

  • 需要动态加载和卸载插件的场景。
  • 动态编译、运行、清理代码。对于网站、脚本引擎等场景十分有用。
  • 用于自检的程序集加载,比如ReflectionOnlyLoad。尽管在很多情况下MetadataLoadContext是更好的选择。

通过MetadataLoadContext读取程序集元数据

我们添加了MetadataLoadContext。它可以在不影响调用者application domain的情况下读取程序集元数据。程序集以数据的形式被读取,包括针对和当前运行时环境不一样的其他架构和平台生成的程序集。MetadataLoadContext和只在.NET Framework中可用的ReflectionOnlyLoad类型有部分重叠。

MetdataLoadContex在System.Reflection.MetadataLoadContext包中可用。它是一个.NET Standard 2.0包。

MetadataLoadContext的应用场景包含了设计时的功能、生产时的工具。以及在运行时需要检查一组程序集并在完成后释放所有内存。

Native Hosting示例

我们的团队发布了一个Native Hosting)的示例。它展示了在native应用程序中寄宿.NET Core的最佳实践方式。

作为3.0的一部分,我们现在为.NET Core宿主暴露了一些通用的功能。这些功能在之前只在官方提供的.NET Core宿主中的.NET Core托管应用程序中可用。这项功能让生成能够充分利用.NET Core的功能的native宿主能够更加简单。

其他API改进

我们对2.1中引入的Span, Memory 以及相关类型进行了优化。像span构造、分片、转换、格式化等常用操作现在有更好的表现。另外像在String用作Dictionary 和其他集合类型的key时,也有了表现上的提升。要获得这些改进不需要改任何的代码。

下面是另外一些改进:

  • HttpClient内置对Brotil的支持
  • ThreadPool.UnsafeQueueWorkItem(IThreadPoolWorkItem)
  • Unsafe.Unbox
  • CancellationToken.Unregister
  • Complex arithmetic operators
  • Socket APIs for TCP keep alive
  • StringBuilder.GetChunks
  • IPEndPoint parsing
  • RandomNumberGenerator.GetInt32
  • System.Buffers.SequenceReader

应用程序现在默认拥有原生可执行文件

.NET Core应用程序现在使用原生的可执行文件生成。对于依赖框架的应用程序是全新的功能。在这之前,只有自包含的应用程序)有可执行程序。

你可以在这些可执行程序上做和原生可执行程序一样的事,比如说:

  • 您可以双击可执行程序启动应用程序。
  • 你可以从命令提示符中启动程序,在Windows上使用myapp.exe,在macOS和Linux上使用./myapp。

在一次build中生成的可执行文件和你的操作系统和CPU是匹配的。比如说如果你用的是Linux x64的机器,那么这个可执行程序也只能在这种机器上运行,而不能在Windows或者Linux ARM的机器上运行。因为这些可执行程序是native code(就像C++)。如果你想以另一种机器为生成目标,那么你就要通过添加运行时参数来发布。如果你喜欢的话,你也可以继续使用dotnet命令启动,而不是使用这些native可执行程序。

使用ReadyToRun镜像优化你的.NET Core应用程序

你可以通过将你的应用程序程序集编译为ReadyToRun(R2R)格式来降低启动时间。R2R是一种AOT形式,是3.0中一种发布时的可选功能。

R2R二进制文件通过减少在应用程序加载时JIT需要进行的工作来减少提升启动时的性能。这种二进制文件包含了JIT会产生的代码类似的native代码。从而让JIT在对性能要求最高的时候(比如启动时)能够休息一下。由于包含了在某些场景中可能需要的IL的同时,还包含了为了减少启动时间所需要的对应的native代码,因此R2R二进制要更大一些。

要启用ReadyToRun编译:

  • 设置PublishReadyToRun属性为true。
  • 显式使用RuntimeIdentifier发布。

注意:当应用程序的程序集被编译时,产生的native代码是针对特定平台和架构的(这就是你需要制定有效的RuntimeIdentifier的原因)。

下面是一个例子:

<Project Sdk=“Microsoft.NET.Sdk”>
  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>netcoreapp3.0</TargetFramework>
    <PublishReadyToRun>true</PublishReadyToRun>
  </PropertyGroup>
</Project>

然后使用下面的命令发布:

dotnet publish -r win-x64 -c Release

注意:RuntimeIdentifier可以设置成其他操作系统或者芯片类型。也可以在项目文件中设置。

程序集链接

3.0 SDK同时包含了一个通过分析IL和消除无用程序集来减少应用程序大小的工具。这是3.0中另一个发布时的可选功能。

有了.NET Core,您可以不必在部署目标上安装.NET,随时都可以发布包含运行代码所需一切的的自包含应用程序。有些时候应用程序只需要框架的一个很小的子集,如果只包含所用到的库的话,还可以变得更小。

我们使用IL链接器扫描您的程序并检测真正必需的代码,然后剔除掉无用的框架库。这样的操作可以显著减少一些应用程序的大小。特别是工具类的控制台应用程序受益最大。因为这类应用程序很多时候只用了框架中相当小的部分从而易于剔除那些无用的。

要使用链接器:

  • 将PublishTrimmed属性设置为true。
  • 使用显式指定的RuntimeIdentifier发布。

下面是一个例子:

<Project Sdk=“Microsoft.NET.Sdk”>
  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>netcoreapp3.0</TargetFramework>
    <PublishTrimmed>true</PublishTrimmed>
  </PropertyGroup>
</Project>

然后使用下面的命令发布:

dotnet publish -r win-x64 -c Release

注意:RuntimeIdentifier可以设置成其他操作系统或者芯片类型。也可以在项目文件中设置。

发布的输出文件将根据应用程序代码的调用情况,只包含框架库的一个子集。以helloworld为例,链接器将应用程序大小从约68MB减小到了约28MB。

使用到了反射或者相关的动态特性的应用程序或者框架(包括http://ASP.NET Core和WPF)在剔除时经常会出错,因为链接器并不知道这些动态行为,并且通常无法确定在运行时反射需要哪些框架类型。为了给这样的应用程序瘦身,你需要告诉链接器你的代码中、依赖的框架和包中有哪些需要反射的类型。在瘦身后一定要测试您的app。我们正在着手提升这项功能在.NET 5中的体验。

要了解关于IL链接器的更多信息,参阅文档,或者访问mono/linker存储库。

注意:在之前版本的.NET Core中,ILLink.Tasks作为一个外部的NuGet包发布,并且提供了大量相同的功能。不过这个包以后不再受支持了,请更新到3.0 SDK来享受全新的体验!

链接器和R2R编译器可以被用到同一个应用程序上。通常链接器会让你的应用程序个小,而R2R编译器会让它又变大一点,不过会有显著的性能提升。值得测试一下各种不同的配置以便了解各个选项带来的影响。

发布单文件可执行程序

现在你可以使用dotnet publish命令来发布一个单文件可执行程序。这种形式的单文件EXE相当于一个自解压的可执行程序。它将所有的依赖(包括native依赖)作为资源包含其中。在启动时,它会将所有的依赖拷贝到临时目录,并且在那里加载这些依赖。只需要对这些依赖解压一次,从此以后就可以毫无负担地快速启动。

您可以通过在工程文件中添加PublishSingleFile属性,或者在命令行添加一个新的开关参数来启用这个发布选项。

以64位Windows为例,要生成自包含的单文件EXE应用程序:

dotnet publish -r win10-x64 /p:PublishSingleFile=true

注意:RuntimeIdentifier可以设置成其他操作系统或者芯片类型。也可以在项目文件中设置。

在SIngle file bundler中获得更多信息。

程序集瘦身、AOT编译(通过crossgen)、单文件打包都是可以在3.0中单独或者一起使用的新功能。

我们觉得可能比起自解压的可执行程序,你们会更喜欢3.0中通过AOT编译器提供的单文件exe。AOT编译形式的单文件可执行程序会作为.NET 5的一部分发布。

dotnet build现在会拷贝依赖

dotnet build现在会在生成操作进行的时候把您的应用程序的NuGet依赖从NuGet缓存中拷贝到您的输出文件将。在本次发布之前,只有在dotnet publish的时候会把依赖拷贝过来。这项改进让你可以把生成的输出文件拷贝到不同的机器上。

但是像链接和razor page的发布这样的操作都必须进行publish。

.NET Core工具————局部安装

.NET Core工具现在可以局部安装。比起2.1中加入到全局工具,本地工具有它的优势。

局部安装能够:

  • 限制可用工具的作用域。
  • 使用指定版本的工具——这些工具可能和全局安装的或者另一个局部安装的工具版本不一样。本地安装的工具版本一句本局部工具的manifest。
  • 使用dotnet启动,比如dotnet mytool。

注意:参阅Local Tools Early Preview Documentation。

.NET Core安装器会就地更新

Windows的.NET Core SDK MSI安装器会就地更新补丁版本。这会减少在开发和生产设备上安装的SDK数量。

更新规则会指定目标.NET Core SDK特性范围(feature bands)。特性范围由版本号的补丁部分以百位数组成。比如说3.0.101和3.0.201是两个不同的特性范围。而3.0.101和1.0.199是相同的特性范围。

这意味着当3.0.101可用并被安装时,如果本机上有3.0.100的话,那么就会被移除。而当3.0.200在同样的机器上可用并被安装时,3.0.101不会被移除。此时,会默认使用3.0.200,但是如果通过global.json配置的话,3.0.101(或者更高的.1xx版本)仍然可以使用。

这种和global.json对齐的方式可以在不同补丁版本之间roll forward,但是不能跨特性范围。因此通过SDK安装器更新并不会造成SDK丢失的错误。使用VS安装SDK的用户也可以使用VS的端到端安装来对齐特性范围。

更多信息请参阅:

  • .NET Core versioning
  • Remove .NET Core SDK versions

.NET Core SDK大小改进

.NET Core SDK在3.0中要小得多。主要原因是我们改变了构造SDK的方式。现在SDK成为了按需build的各种“包”(引用的程序集、框架、模版)。在之前的版本中(包括2.2),我们都是通过NuGet包构造SDK的,这其中就包含了一些不需要的东西,浪费了很多空间。

3.0 SDK的大小

操作系统 安装器大小(变化)安装后大小(变化) Windows 164MB (-440KB; 0%) 441MB (-968MB; -68.7%)

Linux 115MB (-55MB; -32%) 332MB (-1068MB; -76.2%)

macOS 118MB (-51MB; -30%) 337MB (-1063MB; -75.9%)

Linux和macOS上的大小减少实在是很惊人。Windows上减小得更少是因为我们在3.0中加入了WPF和WIndows Forms的支持。惊人的是即使我们加入了WPF和Windwos Forms,安装器的大小还是减少了一点点。

你可以在.NET Core SDK Docker镜像上看到同样的变化(不过这里仅限x64和Alpine)。

Distro 2.2 Size 3.0 Size

Debian 1.74GB 706MB

Alpine 1.48GB 422MB

你可以在这里.NET Core 3.0 SDK Size Improvements · GitHub了解我们是这样计算文件大小的。其中提供了详细说明以便你可以在自己的环境中进行同样的测试。

Docker发布更新

微软的团队现在在 Microsoft Container Registry (MCR) 上发布容器镜像。出于这两个原因我们进行这样的更改: 将微软提供的容器镜像放到多个地方,比如Docker Hub和Red Hat。 使用Azure作为全球的CDN来分发微软提供弄的容器镜像。

在.NET团队中,我们现在也把所有的.NET Core镜像 发布到MCR。点开链接你就可以看到,我们以后也会在Docker Hub上有我们的“主页”。以后我们也会一直这样。MCR不提供这样的“主页”,而是依靠像Docker Hub这样公开的页面来给用户提供和镜像相关的信息。 像 microsoft/dotnet 和 microsoft/dotnet-nightly 这些我们仓库的链接,现在也转到了新的位置。不过在之前那些地方的镜像以后也不会被删除。

我们会继续在各个版本的.NET Core的生命周期中支持那些旧仓库中的标签。比如说2.1-sdk,、2.2-runtime、 latest。现在已经不再支持2.1.2-sdk 这样的有三个部分的标签。

比如说拉取3.0 SDK镜像的正确的标签做饭吃是下面这样的:

mcr.microsoft.com/dotnet/core/sdk:3.0

新的MCR字符串在docker pull和Dockerfile的FROM命令都会使用。

参阅 .NET Core Images now available via Microsoft Container Registry 获得更多信息。

SDK Docker镜像包含PowerShell Core

PowerShell Core 已经按照 requests from the community 的要求加入到了.NET Core SDK Docker容器镜像中。PowerShell Core是一个跨平台(Windows、Linux、macOS)的自动化、配置工具/框架。它能够和您现有的工具很好地写作,并且已为处理结构化数据(JSON、CSV、XML等等)、REST API、对象模型优化。包括命令行shell、相关的脚本语言、处理cmdlet的框架。

通过下面的命令,你可以尝试容器镜像中的PowerShell Core:

docker run —rm mcr.microsoft.com/dotnet/core/sdk:3.0 pwsh -c Write-Host “Hello Powershell”

在这两个主要场景中,只有容器镜像中的PowerShell可以做到:

  • 使用PowerShell语法编写任何任何操作系统的.NET Core应用程序Dockerfile。
  • 编写能够被轻易容器化的.NET Core应用程序/库的build逻辑/

容器化build时启动PowerShell的示例语法:

  • docker run -it -v c:myrepo:/myrepo -w /myrepo http://mcr.microsoft.com/dotnet/core/sdk:3.0 pwsh build.ps1
  • docker run -it -v c:myrepo:/myrepo -w /myrepo http://mcr.microsoft.com/dotnet/core/sdk:3.0 ./build.ps1

要让第二个例子在Linux上正常工作,.ps1文件需要遵循下面的模式,并且需要使用Unix的LF结尾,而不能是Windows的CRLF。

#!/usr/bin/env pwsh
Write-Host “test”

如果您刚开始接触PowerShell并且想要了解更多,推荐阅读 getting started 文档。

注意:PowerShell Core是 .NET Core 3.0 SDK container images 的一部分。但不是.NET Core 3.0 SDK 的一部分。

对Red Hat的支持

在2015年4月,我们宣布.NET Core将支持Red Hat Enterprise Linux。经过了和Red Hat完美的合作,在2016年六月,.NET Core 1.0作为一个组件出现在了Red Hat Software Collections中。在这次合作中,我们学到了关于在Linux社区中发布软件方面的很多东西(并且我们还会继续学习下去!)。

过去的四年里,Red Hat和微软在同一天提供了许多.NET Core的更新和像2.1、2.2这样的重大更新。有了.NET Core 2.2,Red Hat将.NET Core延伸到了OpenShift平台上。随着RHEL 8的发布,我们激动地告诉您2.1现在已可以通过Red Hat Application Streams获取,3.0页即将到来。

结语

.NET Core 3.0是.NET Core的一次新的主要版本更新,其中包含了大量的更新。推荐您尽快地开始使用3.0。3.0大大地改进了.NET Core的很多方面。比如SDK大小的大幅减小、对容器和Windows桌面应用程序支持方面的改进等等。不过也还有很多没有在本文中提到的小的改进,但是你也会慢慢地从中受益。

请一定要在未来的今天、几周或者几个月中和我们分享您的反馈。希望您喜欢。能为您做到这些十分开心。

如果您想要了解更多,我们推荐您阅读下面这些近期的文章:

  • The Evolving Infrastructure of .NET Core
  • How the .NET Team uses Azure Pipelines to produce Docker Images
  • .NET Core Workers as Windows Services
  • .NET Core and systemd
  • Messaging Practices
  • Visual Studio Tips and Tricks: Increasing your Productivity for .NET

作者:RIchard Lander Program Manager, .NET Team

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值