65.9K
CodeProject 正在变化。 阅读更多。
Home

面向对象开发人员的 TypeScript 简单示例 第 1 部分

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.97/5 (18投票s)

2017年11月29日

CPOL

15分钟阅读

viewsIcon

27435

downloadIcon

154

面向对象程序员的 TypeScript 教程。

引言

随着微软转向 JavaScript 进行 Web 编程,在伟大的 Anders Hejlsberg 的领导下,他们决定创建一个可以编译成 JavaScript 以在所有 Web 浏览器中运行的面向对象语言,但又能为混乱的弱类型、非 OOP Web 编程世界带来一些秩序。

语法上,TypeScript 与 JavaScript 非常相似。事实上,JavaScript 程序无需任何更改即可在 TypeScript 中运行。但是,TypeScript 允许将 Web 编程带入面向对象开发领域。因此,我将 TypeScript 视为一种适配器 - 它将 JavaScript 适配到面向对象概念。

TypeScript 的另一项伟大成就是,它提供了最新的 ECMAScript 6(许多浏览器尚不支持)中的最新特性,并将其转译为早期 JavaScript 版本,例如 ECMAScript 5。因此,使用 TypeScript 开发会自动使您的代码兼容多浏览器和多平台。没有 TypeScript,开发人员将不得不使用其他工具来实现类似的结果,例如 BabelJS。

您在网上找到的大多数 TypeScript 教程都是面向想要学习 TypeScript 并需要了解面向对象概念的 JavaScript 开发人员的。

在这里,我采用了一种不同的方法 - 向面向对象开发人员解释 TypeScript。我的目的是提供非常简单的代码片段,以突出特定功能,以便经验丰富的 Java 或 C# 开发人员几乎无需阅读文字即可理解该语言的功能。

所需背景知识

我假设您对 OOP 概念和一种 OOP 语言(最好是 C# 或 Java)有一定的基本了解。我还期望您对 HTML 编程和 JavaScript 有一些非常基础的了解。对于那些需要复习 HTML 和 JavaScript 的人 - 这是我几年前写的一篇文章:HTML5, JavaScript, Knockout, JQuery, Silverlight/WPF/C# 戒瘾指南。第一部分 - JavaScript 和 DOM。

第一部分和第二部分将涵盖的主题

第一部分涵盖以下主题

  1. 设置环境。
  2. 创建一个简单的 TypeScript "Hello World" 项目。
  3. TypeScript 中的变量和类型。
  4. 带属性的 TypeScript 类。
  5. 在 TypeScript 中扩展类。
  6. 实现接口。
  7. 匿名类。
  8. TypeScript 中的泛型。

 

第二部分应涵盖以下主题(可能会有变动)

  1. 模块和命名空间。
  2. TypeScript 的类似 LINQ 的功能。
  3. JSON 到 TypeScript 映射器。
  4. Promise 和异步编程。
  5. TypeScript 的 RxJS。
  6. JSX

 

开发工具

我尝试了几种开发工具。对于 TypeScript 开发,我喜欢Visual Studio Code。但所有这些工具(包括 Visual Studio Code)都需要安装和配置大量额外的软件。解释所有这些会使本文充斥不必要的细节。所以,对于本文,我决定坚持使用 Visual Studio 2017。也许以后我会写另一篇文章介绍各种 TypeScript 环境。

如上所述,我使用 VS2017 作为开发工具,并在 Chrome 浏览器中运行大多数示例(尽管这可能无关紧要,因为所有 TypeScript 功能都应该是与浏览器无关的)。

设置 VS2017

尽管 VS2017 开箱即用地包含了大多数 TypeScript 功能,但我还是为 VS2017 安装了最新版本的 TypeScript(对我来说是 2.6 版本)。要下载它,您可以直接遵循TypeScript 扩展链接中的链接。

代码位置

本文的代码位于 GitHub 上,网址为:面向 OOP 开发人员的 TypeScript 第一部分代码

TypeScript Hello World 示例

我们将从最小、最简单的 TypeScript 示例开始,它不仅能突出一些 TypeScript 的特性,最重要的是,它将教会我们如何创建和设置使用 TypeScript 的 VS2017 项目。

创建项目,设置属性

我建议将您的 TypeScript 项目创建为“空 ASP.NET Web 应用程序”项目。为此,请从文件菜单中选择“新建”->“项目”选项。在打开的对话框中,单击左侧的“Visual C#”->“Web”选项,然后在主面板中选择“ASP.Net Web Application (.NET Framework)”选项。不要忘记勾选“为解决方案创建目录”复选框;否则,创建的解决方案文件将位于主文件夹中(这可能不是您想要的):

现在,让我们创建名为“Default.html”的主 HTML 文件。右键单击解决方案资源管理器中的项目,然后选择“添加”->“新项”选项。在打开的对话框中,在左侧选择“Visual C#”->“Web”,然后在主面板中选择“HTML Page”。为项目选择名称“Default.html”,然后单击“添加”按钮:

您应该将新创建的 Default.html 文件设置为启动页。为此,请右键单击解决方案资源管理器中的文件,然后选择“设置为启动页”选项:

现在,通过右键单击项目并选择“添加”->“新建文件夹”来为所有 TypeScript 和 JavaScript 文件创建一个项目文件夹。将新文件夹命名为“Scripts”。

通过右键单击“Scripts”文件夹并选择“添加”->“新项”,在该文件夹中创建一个名为 HelloWorld.ts 的 TypeScript 文件。在打开的对话框中,使用顶部的搜索文本框搜索 TypeScript 模板。在中间部分选择“TypeScript File”选项,在底部将文件命名为 HelloWorld.ts,然后单击“添加”按钮:

现在,在 HelloWorld.ts 文件中放入以下代码

class HelloWorldPrinter
{
    static Print(): void
    {
        alert("Hello World!");
    }
}    
  

 

在 Default.html 文件的 body 标签中,添加以下脚本标签

<body>
    <script src="Scripts/HelloWorld.js"></script>
    <script>
        HelloWorldPrinter.Print();
    </script>
</body>

 

您实际上可以通过将 TypeScript 文件拖到 HTML 代码中来添加对其的引用,因此,与其编写 ` <script src="Scripts/HelloWorld.js"></script>`,不如将 HelloWorld.ts 文件拖到 `<body>` 标签内的目标位置。

现在尝试运行应用程序 - 按下“调试”按钮或使用“DEBUG”->“开始调试”菜单项。您的默认浏览器应该会启动(在我的例子中是 Chrome),并在警报窗口中显示“Hello World!”文本。

请注意,您可以在 HelloWorld.ts 文件中设置断点,并且它会被命中。多年前,Visual Studio 调试器只能与 MS Internet Explorer 浏览器一起使用,现在,显然它可以与其他浏览器一起使用,至少与 Chrome 一起使用。

启动应用程序的另一种方法是右键单击 Default.html 文件并选择“在浏览器中查看”菜单选项。但是,在这种情况下,您将无法调试 TypeScript(您仍然可以在浏览器中使用 JavaScript 调试器,但它只能在生成的 JavaScript 上工作,而不能在原始 TypeScript 上工作)。

现在,让我们看看项目的属性。特别地,我们关心“TypeScript Build”选项卡。

  <img src="1217899/ProjProps.png" class="" alt="" />

请注意,“保存时编译”选项确保每次开发人员保存 TypeScript 文件时都会尝试生成 JavaScript 文件。

勾选“生成源映射”会在 JavaScript 文件与 TypeScript 之间生成映射文件,以允许 TypeScript 调试。

另一个有趣的属性是“ECMAScript 版本”。此属性指定 TypeScript 编译成的 JavaScript 版本。“ECMAScript 5”是一个非常合理的选择,因为大多数浏览器都支持它,而较新的选项(例如“ECMAScript 6”)支持程度不高。

这三个属性很重要,但 VS2017 默认情况下会为您设置正确的值。

代码

让我们再看一下 HelloWorld.ts 代码

class HelloWorldPrinter
{
    static Print(): void
    {
        alert("Hello World!");
    }
}    

与 C# 不同,您不需要在类或方法前加上 "public" 关键字 - 类和方法默认是公共的。

与 C# 或 Java 一样,TypeScript 拥有类,方法可以定义为静态(使用 "static" 关键字)或实例(不使用任何关键字)。

公共静态方法可以通过类名在类外部访问,正如我们在 Default.html 文件中所做的那样。

HelloWorldPrinter.Print();

几个重要说明

在 HelloWorld 示例中,我特意涵盖了创建项目、创建文件、更改项目属性的步骤。在本文的其余部分,我将专注于代码本身。

TypeScript 中的大多数基本编程语言构造都与 JavaScript 相似,而 JavaScript 又从 Java 中借用了它们,因此 `if`、`while` 和 `for` 以及其他构造与 C++、Java 和 C# 中的非常相似。

在其余的示例中,我将专注于 TypeScript 特有的功能和最佳实践,省略那些与 JavaScript 或上述 OO 语言相似的。例如,我不会描述 `while` 循环,因为它与上述语言中的完全相同。

变量

变量示例的代码位于 Variables/Variables.sln 解决方案下。请注意,代码并未编译 - 我特意引入了一些编译错误,以展示 TypeScript 中会导致错误的情况。

我们感兴趣的 TypeScript 文件位于项目文件夹 Scripts 下

  1. BasicTypeVariables.ts
  2. EnumVariables.ts
  3. ArrayVariables.ts
  4. TupleVariables.ts

 

基本类型

基本类型在 BasicTypeVariables.ts 文件中进行了演示。这是它的内容:

// define number
let myNumber: number;

// define string
let myString: string;

// define any (can take any type
// - analogous to object in C#)
let myAny1: any = null;

// no type specified analogous to 'any'
let myAny2; 

// initialised to boolean
// even though the type is not specified
// you cannot change the type later
let myBool1 = true;

// type specified and initialize to false
let myBool2: boolean = false;

// compiler error
// cannot reassign the type to 'number'
myString = 5; 

myAny1 = 5; //OK

// OK
myAny2 = "hello world"; 

// any can be reassigned any type (like object in C#)
myAny1 = "Hi";

// casting:
// the two expressions below 
// are equivalent.
myAny2 as string;
(<string>myAny2);  

代码注释应该能让您大致了解情况。

请注意,为了定义一个变量,我们需要使用关键字 "let"(与 JavaScript 中的 "var" 不同)。我们也可以在这里使用 "var",但那样的话,我们的作用域就会被搞乱 - 就像在常规 JavaScript 中一样。然而,关键字 "let" 将定义变量的作用域限制在花括号包围的块内 - 这与主要的 OO 语言中的做法相同,也应该是这样的。

变量的类型可以在变量名后的冒号后指定。有几种基本类型,包括 **number**、**string** 和 **boolean**。还有一个特殊的类型 **any**。这种类型的变量可以 Assume 任何其他类型。类型 **any** 类似于 Java 或 C# 中的 **object** 类型。如果未为变量分配类型且未对其进行初始化,则假定其类型为 **any**。

如果变量未分配类型,但在声明行中初始化为某种 **number**、**string** 或 **boolean**,TypeScript 会假定其被定义为相应类型。

一旦 TypeScript 变量被假定为某种(非 **any**)类型,它就不能被赋予任何其他类型的值 - 编译器将报告错误。

枚举

可以使用与 C# 非常相似的方式在 TypeScript 中定义枚举。这是 EnumVariables.ts 文件的内容:

enum Season
{
    Winter,
    Spring = 1, // can assign integer 
    Summer,
    Fall
}

let summerNumber: number;

// summerNumber = 2
summerNumber = Season.Summer; 

let summerName: string;

// summerName = "Summer"
summerName = Season[Season.Summer];  

这里唯一的棘手之处是检索枚举的名称。可以通过数组索引表示法获取:`summerName = Season[Season.Summer];`。

数组

ArrayVariables.ts 文件展示了定义和初始化数组(或列表或向量)的方法:

let arrayOfStrings = ["str1", "str2", "str3"];

// myStr="str1"
let myStr = arrayOfStrings[0]; 

// compiler error
// cannot assign an array of numbers
// to an array of string.
arrayOfStrings = [1, 2, 3];

// can contain any types like List<object> in C#
let arrayOfAny: any[] = [1, "str2", false];

// for any[] reassigning to an array 
// containing different types works fine
arrayOfAny = [true, 2, "str3"];

// another way to define array of strings
// by using Array<string>
let arrayOfStrings2: Array<string>;

// now arrayOfStrings2 contains ["str1", "str2", "str3", "str4"]
arrayOfStrings2 = [...arrayOfStrings, "str4"];

let i = 5;  

上面代码中不易理解的部分是包含所谓展开运算符 "..." 的部分。

arrayOfStrings2 = [...arrayOfStrings, "str4"];  

展开运算符在语法上解包数组,将其转换为逗号分隔的成员。它在这里用于数组连接,也可用于创建具有可变数量输入参数的方法 - 类似于 C# 中的 **params** 关键字。

元组

这是 TupleVariables.ts 文件的内容:

let myTuple: [number, string, boolean];

myTuple = [123, "123", true]; // works fine

myTuple = [123, 123, 123]; // compiler error  

这里没有什么不寻常的。

类和接口

带属性的类的示例

Solution ClassAndProperties.sln 包含一个 `Person` 类的示例,该类有三个属性。

class Person
{
    //#region
    // read-write property FirstName
    private _firstName: string;
    get FirstName(): string
    {
        return this._firstName;
    }
    set FirstName(value: string)
    {
        this._firstName = value;
    }
    //#endregion

    //#region
    // read-write property LastName
    private _lastName: string;
    get LastName(): string
    {
        return this._lastName;
    }
    set LastName(value: string)
    {
        this._lastName = value;
    }
    //#endregion

    // read-only property FullName
    get FullName(): string 
    {
        let result: string = "";

        if (this.FirstName) 
        {
            result += this.FirstName;
            result += " ";
        }

        if (this.LastName)
        {
            result += this.LastName;
        }

        return result;
    }

    // constructor taking two optional arguments
    constructor(firstName?: string, lastName?: string)
    {
        this.FirstName = firstName;
        this.LastName = lastName;
    }
}  

`FirstName` 和 `LastName` 属性是读写属性,而 `FullName` 属性是只读属性。

让我们仔细看看其中一个属性:

    //#region
    // read-write property FirstName
    private _firstName: string;
    get FirstName(): string
    {
        return this._firstName;
    }
    set FirstName(value: string)
    {
        this._firstName = value;
    }
    //#endregion  

属性 `FirstName` 以私有字段 `_firstName` 作为其后备存储。getter 和 setter 在类作用域内定义为两个独立的方法(这与 C# 中的做法不同)。不幸的是(也与 C# 不同),您可以为同一属性的 getter 和 setter 设置不同的隐私设置:例如,将 `FirstName` 的 setter 设置为私有或受保护将导致编译器错误。

类的另一个有趣部分是构造函数:

// constructor taking two optional arguments
constructor(firstName?: string, lastName?: string)
{
    this.FirstName = firstName;
    this.LastName = lastName;
}  

变量名后的问号表示参数是可选的,即如果未传递,则假定其处于未定义状态。

TypeScript 不允许方法或构造函数重载,但它允许使用可选参数或带有默认值的参数。当然,这些参数应该始终放在参数列表的末尾。这种功能允许我们几乎匹配 OO 语言中的方法和构造函数重载。

为了以后不再回头讨论,这里有一个类似的构造函数示例,它使用默认值参数而不是可选参数:

constructor(firstName: string = "Unknown", lastName: string = "Unknown")
{
    this.FirstName = firstName;
    this.LastName = lastName;
}  

另外,请注意,方法、属性和字段默认是公共的,但如果您想限制它们的可见性,则需要在变量名前加上 "protected" 或 "private" 关键字。

现在看看 Person.ts 文件的底部:

另一个需要注意的重要事项是,在类内部,类的方法、属性和字段只能通过 "this" 关键字访问。如果您尝试删除它们前面的 "this.",编译器将报告错误。

let people: Array<Person>;

people = [new Person("Joe", "Doe"), new Person("Jane", "Dane")];

let result: string = "";

for (let person of people)
{
    result += person.FullName + "\n";
}

alert(result);  

我创建了一个全局作用域变量 `people`,类型为 `Array<Person>;`。然后我为其分配了一个包含两个 `Person` 对象的数组。现在,看看对数组项的迭代:

for (let person of people)
{
...
}  

这等同于 C# 的 `foreach(var person in people)` 迭代器。

如果运行示例,它将在警报对话框中显示这两个人的全名。

扩展类

Solution ExtendingClass.sln 展示了如何在 TypeScript 中扩展类。

此解决方案中的 `Person` 类与上一个类中的完全相同,但我们也引入了 `Employee` 类,该类扩展了 `Person` 类。

class Employee extends Person
{
    private _employeeNumber: number;
    get EmployeeNumber()
    {
        return this._employeeNumber;
    }

    // property overriding sample
    get LastName() 
    {
        return "SuperCaleFlageolisticExpialodocios";
    }

    constructor(employeeNumber:number, firstName:string, lastName:string)
    {
        // call constructor of the super class
        super(firstName, firstName);

        this._employeeNumber = employeeNumber;
    }
}  

它引入了一个只读属性 `EmployeeNumber`,由私有字段 `_employeeNumber` 支持。

另外,请注意,TypeScript 中的所有方法和属性都是虚拟的,在子类中定义同名属性或方法会自动覆盖它们(无需使用 "override" 关键字)。为了证明这一点,我重写了 `Employee` 对象的 `LastName` 属性的 getter,使其始终返回“SuperCaleFlageolistExpialodocios”,而不是 `LastName` 的 getter。

// property overriding sample
get LastName() 
{
    return "SuperCaleFlageolisticExpialodocios";
}  

现在,看看 Employee.ts 文件的底部:

// function that takes Person object
// and displays it within alert dialog
function PrintPersonFullName(person: Person): void
{
    alert(person.FullName);
}

// create Employee object
let joeDoeEmployee: Person = new Employee(1, "Joe", "Doe");

// call PrintPersonFullName function on
// the joeDoeEmployee object
PrintPersonFullName(joeDoeEmployee);

我们定义了一个函数 `PrintPersonFullName`,它接受一个 `Person` 对象,但我们传递了一个 `Employee` 对象。该函数只是在警报对话框中显示对象的 `FullName` 属性。我们将看到的完整名称是“Joe SuperCaleFlageolisticExpialodocios”,而不是“Joe Doe”,这意味着 `LastName` 属性是虚拟的并且被重写了。

另一个有趣的事情是在构造函数中调用 `super(...)` 方法。它调用超类(父类)的构造函数。

constructor(employeeNumber:number, firstName:string, lastName:string)
{
    // call constructor of the super class
    super(firstName, firstName);

    this._employeeNumber = employeeNumber;
}  

这等同于 Java 中的 `super` 或 C# 中的 `base`。

最后,看看我们的 Default.html,脚本定义的位置:

<body>
    <script src="Scripts/Person.js"></script>
    <script src="Scripts/Employee.js"></script>
</body>  

请注意,我们必须按照它们被使用的顺序导入脚本。在导入 Person.js 之前导入 Employee.js 将导致错误。我们稍后将对此进行更详细的讨论。

实现接口

查看 ImplementingInterface 项目,位于 Scripts 文件夹下。它演示了如何实现多个接口。

`IFirstNameContainer` 接口包含 `FirstName` 属性。

interface IFirstNameContainer
{
    FirstName: string;
}  

`ILastNameContainer` 接口包含 `LastName` 属性。

interface ILastNameContainer
{
    LastName: string;
}  

`IMiddleNameContainer` 包含 `MiddleName` 属性。

interface IMiddleNameContainer
{
    // '?' makes it optional property
    MiddleName?: string;
}  

请注意,'?' 问号使属性成为可选的,即接口不会“坚持”类必须拥有它。

现在,`IPerson` 接口实现了上面列出的所有三个接口。

interface IPerson
    extends
    IFirstNameContainer,
    ILastNameContainer,
    IMiddleNameContainer
{

}  

我们的 `Person` 类实现了 `IPerson` 接口。

class Person implements IPerson
{
    //#region
    // read-write property FirstName
    private _firstName: string;
    get FirstName(): string
    {
        return this._firstName;
    }
    set FirstName(value: string)
    {
        this._firstName = value;
    }
    //#endregion

    //#region
    // read-write property LastName
    private _lastName: string;
    get LastName(): string
    {
        return this._lastName;
    }
    set LastName(value: string)
    {
        this._lastName = value;
    }
    //#endregion

    // constructor taking two optional arguments
    constructor(firstName?: string, lastName?: string)
    {
        this.FirstName = firstName;
        this.LastName = lastName;
    }
}  

请注意,`Person` 没有实现 `MiddleName` 属性,但 TypeScript 仍然没有显示任何问题,因为该属性是可选的。

在 Person.ts 文件的底部,我使用箭头(lambda)表示法定义了一个名为 `PrintFullName` 的函数,而不是使用通常的 JavaScript 方式。

// defining a method (function)
// using error notations
let PrintFullName = (person: IPerson) =>
{
    let result: string = "";

    if (person.FirstName)
    {
        result += person.FirstName + " ";
    }

    if (person.MiddleName)
    {
        result += person.MiddleName + " ";
    } 

    if (person.LastName)
    {
        result += person.LastName;
    }

    alert(result);
} 

最后,我调用此函数在警报对话框中显示全名。

PrintFullName(new Person("Joe", "Doe"));  

请注意,我本可以将 `PrintFullName` 函数定义为接受接口的有趣组合:

let PrintFullName = (person: IFirstNameContainer & ILastNameContainer & IMiddleNameContainer)...  

这是一个很棒的功能 - C# 中没有。总的来说,我们不必定义联合接口,我们可以要求方法参数同时满足多个接口。

另一件有趣的事情是,我们的 `Person` 类实际上不必实现接口,以便我们的方法 `PrintFullName` 仍然可以工作(尽管该方法是为与接口一起工作而构建的,而不是与类对着干)。您可以注释掉 `implements IPerson` 部分,并验证一切仍然正常工作。这是因为编译器将检查接口所需的所有属性是否仍然存在,并将允许编译继续进行。

匿名类

我在 AnonymouseClasses.sln 解决方案中演示了匿名类的功能(顺便说一句,C# 中没有此功能,但它是 Java 的一部分)。

此项目中的 `IPerson` 接口非常简单。

interface IPerson
{
    FirstName: string;

    LastName: string;
}  

现在看看 Person.ts 文件的顶部:

// create anonymous class
let person = new class implements IPerson
{
    private _firstName: string;
    get FirstName(): string
    {
        return this._firstName;
    }
    set FirstName(value: string)
    {
        this._firstName = value;
    }


    private _lastName: string;
    get LastName(): string
    {
        return this._lastName;
    }
    set LastName(value: string)
    {
        this._lastName = value;
    }

    constructor(firstName: string, lastName: string)
    {
        this.FirstName = firstName;
        this.LastName = lastName;
    }
}("Joe", "Doe"); // call constructor  

我们创建一个匿名类对象,该对象实现 `IPerson` 接口,并在创建对象的同时提供实现。

Generics

GenericsSample.sln 提供了一个非常简单的使用泛型的示例。

总的来说,我们为一个实现 `IUniqueIdInterface` 的对象创建了一个非常原始的缓存。

interface IUniqueIdContainer
{
    readonly UniqueId: string;
}  

这是缓存的代码:

class UniqueIdContainerItemsCache<T extends IUniqueIdContainer>
{
    private _mapObject = new Object();

    // add new item to the cache
    // throw exception if such id already exists
    AddItem(item: T)
    {
        if (this._mapObject.hasOwnProperty(item.UniqueId))
        {
            throw "item with the same id already exists in cache";
        }

        this._mapObject[item.UniqueId] = item;
    }

    // get item by id. 
    // if no item with such id exists, return null
    GetItem(uniqueId: string) : T
    {
        if (this._mapObject.hasOwnProperty(uniqueId))
        {
            return this._mapObject[uniqueId] as T;
        }

        return null;
    }
}  

我们定义了 `IUniqueIdContainer` 接口的一个具体实现。

// UniqueObject class implements IUniqueContainer 
// interface
class UniqueObject implements IUniqueIdContainer
{
    UniqueId: string;

    constructor(uniqueId: string)
    {
        this.UniqueId = uniqueId;
    }
}  

然后我们为此类对象创建一个具体的缓存。

// define a cache storing UniqueObjects 
let uniqueObjectCache = new UniqueIdContainerItemsCache<UniqueObject>();  

最后,我们向缓存添加一个项目并从中获取一个项目进行测试。

// add an item to the cache
uniqueObjectCache.AddItem(new UniqueObject("1"));

// get an item from the cache by id
let objFromCache = uniqueObjectCache.GetItem("1");  

摘要

本文的目的是为具有面向对象背景的开发人员提供 TypeScript 教程。

在这里,我讨论了该语言的核心 OO 功能。在下一部分中,我希望讨论一些更深入、更复杂的范例,包括从 JSON 映射和异步编程。

历史

于 2017 年 12 月 3 日对语法和拼写进行了改进。

© . All rights reserved.