2

编程与类型系统:类型简介

 2 years ago
source link: https://zhuanlan.zhihu.com/p/412520863
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

编程与类型系统:类型简介

文章内容来源于《编程与类型系统》(《Programming with Types》),本文作为分享总结类型简介、基本类型这二个章节的要点。

为什么存在类型

在底层的硬件机器中,代码和数据都是0和1,类型首要的作用是区分是代码还是数据。不同类型执行的方式不同,执行方式错误可引发严重错误。

console.log(eval("40+2"));  // 40+2 是一个表达式,可以执行,在控制台打印 42

// 试图将数据解释为代码
console.log(eval("Hello world!"));  // 引发 SyntaxError: Unexpected identifier 错误

如何解释一条数据

相同的一段01位序列数据,使用不同的方式解析,可赋予数据不同的意义。

011000010101000110 可以表示为无符号16位整数49827、带符号整数-15709、UTF-8 编码字符’£’、等等。

v2-f4a70fa82cdfd0602ef0c17ea44402ae_720w.jpg可用不同的方式来解释一个位序列

类型和类型系统

类型:一种数据的分类,定义了允许取值的集合(通常还包括数据可执行的操作、数据的意义)。

例如:布尔值类型取值为 truefalse ;Int16 值类型表示值介于-32768 到+32767 之间的有符号整数集合。

类型系统:一组规则,为编程语言的元素分配和实施类型。这些元素可以是变量、表达式、函数和其他高级结构。

例如:声明定义该元素是会是存放字符串、数字数组、函数、等;或者像 JavaScript 中使用 let,在运行时候会判断元素是什么类型;

或者由类型系统隐式的判断该元素的类型。

类型系统的优点

类型系统的主要优点在于正确性、不可变性、封装、可组合性和可读性。

正确性

function scriptAt(s: string): number {  // 参数类型为 string
    return s.indexOf("Script");
}

console.log(scriptAt("TypeScript"));
console.log(scriptAt(42));  // 由于参数类型不匹配,这行代码在编译时会报错

// Argument of type '42' is not assignable to parameter of type 'string'

不可变性

function safeDivide(): number {
    const x: number = 42;  // 使用关键字 const 来声明不可变的常量

    if (x == 0) throw new Error("x should not be 0");

    x = x - 42;  // 因为 x 是不可变的,不能被重新复制,这行代码在编译时会报错

    return 42 / x;
}

// Cannot assign to 'x' because it is a constant.

封装

class SafeDivisor {
    private divisor: number = 1;  // 使用 private 关键字来声明是私有变量

    setDivisor(value: number) {
        if (value == 0) throw new Error("Value should not be 0");

        this.divisor = value;
    }

    divide(x: number): number {
        return x / this.divisor;
    }
}

function exploit() {
    let sd = new SafeDivisor();

    sd.divisor = 0; // 因为 divisor 是私有变量,不能被外部使用,这行代码在编译时会报错
    sd.divide(42);
}

可组合性

// 使用泛型类型,抽象类型,不必为每种类型都写一个函数
function first<T>(range: T[], p: (elem: T) => boolean)
    : T | undefined {
    for (let elem of range) {
        if (p (elem)) return elem;
    }
}

function findFirstNegativeNumber(numbers: number[])
    : number | undefined {
    return first(numbers, n => n < 0);
}

function findFirstOneCharacterString(strings: string[])
    : string | undefined {
    return first(strings, str => str.length == 1);
}

可读性

// 使用类型申明和泛型,可以清楚知道该函数的实参和返回数据类型
declare function first<T>(range: T[],
    p: (elem: T) => boolean): T | undefined;

类型系统的类型

相比最底层的类型区分代码和数据,现代类型系统之间的主要区别在于检查类型的时机以及检查的严格程度。

静态类型和动态类型

静态类型在编译时检查类型,动态类型则将类型检查推迟到运行时。

静态类型能更快的发现程序中的错误,让程序更加健壮,这是业界的共识;动态类型则更加灵活,然而对编写程序的人则要求更高。

// 动态类型,未使用 TypeScript 情况下的 JavaScript
function quacker(duck) {
    duck.quack();
}

quacker({ quack: function () { console.log("quack"); } });
quacker(42);  // 这行代码将在运行时报错:Uncaught TypeError: duck.quack is not a function

// 静态类型,使用 Typescript 声明一个 Duck 接口,并在函数参数中声明该类型
interface Duck {
    quack(): void;
}

function quacker(duck: Duck) {
    duck.quack();
}

quacker({ quack: function () { console.log("quack"); } });
quacker(42);  // 这行代码将在编译时报错:Argument of type ‘42’ is not assignable to parameter of type ‘Duck’.

弱类型和强类型

这二个术语并没有非常明确的定义,都是约定俗称,表示对一个类型系统的强度衡量。

强类型系统只会做很少的(甚至不做)隐式类型转换,而弱类型系统则允许更多隐式类型转换。

// 弱类型,未指定变量类型,对于字符串和数字类型,在运算时会自动做隐式类型转换
const a = "hello world";
const b = 42;

console.log(a == b);  // false, 在 JavaScript 允许不同类型进行比较
console.log("42" == b);  // true, 在 JavaScript 中 == 运算符会隐式的将二边的值转化为相同类型
console.log("42" === b);  // false, === 运算符会对比是否不同类型

// 强类型,使用 Typescript 声明具体的类型
const a: string =c"hello world";
const b: number = 42;

console.log(a == b);  // 编译时报错:Typescript 不允许不同类型进行比较
console.log("42" == b);  // 编译时报错:Typescript 不允许不同类型进行比较
console.log("42" === b);  // 编译时报错:Typescript 不允许不同类型进行比较

类型推断

在一些静态类型系统中,有些值的类型并不需要显式的指定,类型系统在编译时可以自动推断出。

function add(x: number, y: number) {  // 函数并没有指定返回值类型,但编译器可以自动推断出为 number 类型
    return x + y;
}

let sum = add(40, 2);  // sum 并没有显式的指定为 number 类型,但编译器可以自动推断出

常见的基本类型分为以下几种,根据类型的定义,这些类型为数据赋予了意义、限制了值的取值范围、并为组合这些类型规定了一组规则。

  • 空类型(empty)
  • 单元类型(unit)
  • 布尔值(booleans)
  • 数字(numbers)
  • 字符串(strings)
  • 数组(arrays)
  • 引用类型(references)

空类型代表不能有任何值的类型,其取值范围为空集合。

// 在 TypeScript 中 nerver 可以代表空类型
function raiseError(message: string): never {  // 该函数只会抛出错误,不会有任何的返回值,因为抛错时程序已经终止
    console.error(`Error "${message}" raised`);
    throw new Error(message);
}

单元类型代表没有任何意义的值类型,其取值范围为一个没有意义的值。

// 在 TypeScript 中 void 可以代表单元类型
function greet(): void {  // 该函数只会打印一段字符串,并不会返回任何有意义的值
    console.log("Hello world!");
}

greet();

空类型和单元类型的区别在于,返回单元类型的函数执行完会继续执行下面的程序,而返回空类型的函数程序会停止运行。

布尔值是一个有二个取值的数据类型,其二个取值为 truefalse

提供了三种运算符,&& 代表 AND,|| 代表 OR,! 代表NOT。下表是truefalse 进行三种运算的计算结果。

数字分为无符号整数、有符号整数、浮点数、大数。

无符号整数

例如一个4位无符号整数可以表示0~15之间的任意值。

4位无符号整数编码。最小取值为0,即全部4个位均为0。最大取值为15,即全部位均为 1(1×23 + 1×22 + 1×21 + 1×20)

有符号整数

一个4位带符号整数可表示–8~7之间的值。

4位带符号整数编码。–8被编码为24 – 8(二进制为1000),–3被编码为24 – 3(二进 制为1101)。对于负数,第一位总是1,对于正数,第一位总是0

浮点数

IEEE 754 是美国电气和电子工程师协会为表示浮点数(带小数部分的数字)制定的标准。

0.10的浮点数表示。最上面显示了浮点数的三个部分(符号位、指数和尾数)在内存中的二 进制表示。中间显示了将二进制表示转换为数字的公式。最下面显示了应用公式的结果:0.1被近似 为0.100000000000000005551115123126

因为 IEEE 754 表示浮点数的方式,JS 中直接比较浮点数会导致问题。JS 中提供了一个值叫 Number.EPSILON (最大圆整误差),只要比较二个值的差值小于或等于这个误差则可以认定为相等。

function epsilonEqual(a: number, b: number): boolean {
    return Math.abs(a - b) <= Number.EPSILON;  // 检查二个数是否在最大圆整误差内
}

console.log(0.1 + 0.1 + 0.1 == 0.3);  // false,因为JS中处理浮点数的误差,0.1+0.1+0.1为0.30000000000000004
console.log(epsilonEqual(0.1 + 0.1 + 0.1, 0.3));  // true,因为0.3和0.30000000000000004在最大圆整误差内

字符串是一个有无限个值的基本类型,其取值为0个或多个字符组成。

一个字节有八个二进制位,00000000到11111111最多可以表示为256个字,也就是最初的 ACSII 码,到现在的 Unicode 可以表示超过百万个字符,包括全世界的字符和表情字符。

对于正常的文本,我们可以很方便的对文本做任何的操作,对于表情等特殊字符,则需要了解更多的字符编码知识。

例如女警官表情符号 ‍♀️ 在 JavaScript 中则由5个字符表示, ‍♀️.length 返回5。尝试拆分这个符号则发现这个符号由警官表情符号 和女性表情符号♀组成。这二个表情符号由\u200d 零宽连接字符组成。整个女警官表情符号为5个Unicode转义字符组成:\ud83d\udc6e\u200d\u2640\ufe0e

使用UTF-16字符串编码的内存中的位、UTF-16字节序列、Unicode代码点序列和书写位的形 式表示女警官表情符号

数组和引用

使用数组和引用可以构建更高级的数据结构,例如列表和树等。

默认把固定长度和数据类型的数组视为基本类型。数据存储在一个连续的内存区域,并且存储了相同位数的值类型,使得访问数组中任何一个值的速度都很快。

将5个32位整数存储在一个固定大小数组中。在固定大小数组中查询元素非常快,因为我们能够计算出它的准确位置。

引用类型则并不保存实际的值,存放实际值的地址,根据该地址则可以快速找到该值。如果二个引用类型的对象,保存着相同的地址,则根据一个对象改变该值的数据,则另外一个引用类型中的值也可以看到做出的修改。

将5个32位整数存储在一个链表中。链表要求我们沿着next元素前进,直到找到要寻找的元素。元素可能存储在内存中的任何位置。

利用数组和引用类型可以构建更高级的数据结构内容比较多,有些晦涩,大家可自行找资料阅读。

基础知识非常枯燥,可以坚持看到最后的同学非常难得。

类型系统在大学的计算机课程中肯定会接触到,本文则记录分享扩展一些基本知识。

JavaScript 从一开始缺乏类型系统,运行时错误满天飞,在 TypeScript 类型系统的加持下,我们可以编写出更好、更加安全的代码。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK