来一起探索 TypeScript 里的通用类型吧
介绍
泛型是静态类型语言的基本特征,允许开发人员将类型作为参数传递给另一种类型、函数或其他结构。当开发人员使他们的组件成为通用组件时,他们使该组件能够接受和强制在使用组件时传入的类型,这提高了代码灵活性,使组件可重用并消除重复。
TypeScript 完全支持泛型,以此将类型安全性引入到接受参数和返回值的组件中,这些参数和返回值的类型,在稍后的代码中使用之前是不确定的。在今天的内容中,我们将尝试 TypeScript 泛型的真实示例,并探索它们如何在函数、类型、类和接口中使用。
我们还将使用泛型创建映射类型和条件类型,这将帮助我们创建可以灵活应用于代码中所有必要情况的 TypeScript 组件。
准备工作
介绍
TypeScript 是 JavaScript 语言的扩展,它使用 JavaScript 运行时和编译时类型检查器。
TypeScript 提供了多种方法来表示代码中的对象,其中一种是使用接口。TypeScript 中的接口有两种使用场景:您可以创建类必须遵循的约定,例如,这些类必须实现的成员,还可以在应用程序中表示类型,就像普通的类型声明一样。
您可能会注意到接口和类型共享一组相似的功能。
事实上,一个几乎总是可以替代另一个。
主要区别在于接口可能对同一个接口有多个声明,TypeScript 将合并这些声明,而类型只能声明一次。您还可以使用类型来创建原始类型(例如字符串和布尔值)的别名,这是接口无法做到的。
TypeScript 中的接口是表示类型结构的强大方法。它们允许您以类型安全的方式使用这些结构并同时记录它们,从而直接改善开发人员体验。
在今天的文章中,我们将在 TypeScript 中创建接口,学习如何使用它们,并了解普通类型和接口之间的区别。
我们将尝试不同的代码示例,可以在 TypeScript 环境或 TypeScript Playground(一个允许您直接在浏览器中编写 TypeScript 的在线环境)中遵循这些示例。
准备工作
要完成今天的示例,我们将需要做如下准备工作:
- 一个环境。我们可以执行 TypeScript 程序以跟随示例。要在本地计算机上进行设置,我们将需要准备以下内容。
- 为了运行处理 TypeScript 相关包的开发环境,同时安装了 Node 和 npm(或 yarn)。本文教程中使用 Node.js 版本 为14.3.0 和 npm 版本 6.14.5 进行了测试。要在 macOS 或 Ubuntu 18.04 上安装,请按照如何在 macOS 上安装 Node.js 和创建本地开发环境或如何在 Ubuntu 18.04 上安装 Node.js 的使用 PPA 安装部分中的步骤进行操作。如果您使用的是适用于 Linux 的 Windows 子系统 (WSL),这也适用。
- 此外,我们需要在机器上安装 TypeScript 编译器 (tsc)。为此,请参阅官方 TypeScript 网站。
- 如果你不想在本地机器上创建 TypeScript 环境,你可以使用官方的 TypeScript Playground 来跟随。
- 您将需要足够的 JavaScript 知识,尤其是 ES6+ 语法,例如解构、rest 运算符和导入/导出。如果您需要有关这些主题的更多信息,建议阅读我们的如何用 JavaScript 编写代码系列。
- 本文教程将参考支持 TypeScript 并显示内联错误的文本编辑器的各个方面。这不是使用 TypeScript 所必需的,但确实可以更多地利用 TypeScript 功能。为了获得这些好处,您可以使用像 Visual Studio Code 这样的文本编辑器,它完全支持开箱即用的 TypeScript。你也可以在 TypeScript Playground 中尝试这些好处。
本教程中显示的所有示例都是使用 TypeScript 4.2.3 版创建的。
泛型语法
在进入泛型应用之前,本教程将首先介绍 TypeScript 泛型的语法,然后通过一个示例来说明它们的一般用途。
泛型出现在尖括号内的 TypeScript 代码中,格式为 <T>,其中 T 表示传入的类型。<T> 可以理解为 T 类型的泛型。
在这种情况下,T 将以与函数中参数相同的方式运行,作为将在创建结构实例时声明的类型的占位符。因此,尖括号内指定的泛型类型也称为泛型类型参数或只是类型参数。多个泛型类型也可以出现在单个定义中,例如 <T, K, A>。
注意:按照惯例,程序员通常使用单个字母来命名泛型类型。这不是语法规则,你可以像 TypeScript 中的任何其他类型一样命名泛型,但这种约定有助于立即向那些阅读你的代码的人传达泛型类型不需要特定类型。
泛型可以出现在函数、类型、类和接口中。本教程稍后将介绍这些结构中的每一个,但现在将使用一个函数作为示例来说明泛型的基本语法。
要了解泛型有多么有用,假设您有一个 JavaScript 函数,它接受两个参数:一个对象和一个键数组。该函数将基于原始对象返回一个新对象,但仅包含您想要的键:
function pickObjectKeys(obj, keys) {
let result = {}
for (const key of keys) {
if (key in obj) {
result[key] = obj[key]
}
}
return result
}
此代码段显示了 pickObjectKeys() 函数,该函数遍历keys数组并使用数组中指定的键创建一个新对象。
下面是一个展示如何使用该函数的示例:
const language = {
name: "TypeScript",
age: 8,
extensions: ['ts', 'tsx']
}
const ageAndExtensions = pickObjectKeys(language, ['age', 'extensions'])
这声明了一种对象,然后使用 pickObjectKeys() 函数隔离 age 和 extensions 属性。ageAndExtensions 的值如下:
{
age: 8,
extensions: ['ts', 'tsx']
}
如果要将此代码迁移到 TypeScript 以使其类型安全,则必须使用泛型。我们可以通过添加以下突出显示的行来重构代码:
function pickObjectKeys<T, K extends keyof T>(obj: T, keys: K[]) {
let result = {} as Pick<T, K>
for (const key of keys) {
if (key in obj) {
result[key] = obj[key]
}
}
return result
}
const language = {
name: "TypeScript",
age: 8,
extensions: ['ts', 'tsx']
}
const ageAndExtensions = pickObjectKeys(language, ['age', 'extensions'])
<T, K extends keyof T> 为函数声明了两个参数类型,其中 K 被分配一个类型,该类型是 T 中的key的并集。
然后将 obj 函数参数设置为 T 表示的任何类型,并将key设置为数组, 无论 K 代表什么类型。
由于在语言对象的情况下 T 将 age 设置为数字并将 extensions 设置为字符串数组,因此,变量 ageAndExtensions 现在将被分配具有属性 age: number 和 extensions: string[] 的对象的类型。
这会根据提供给 pickObjectKeys 的参数强制执行返回类型,从而允许函数在知道需要强制执行的特定类型之前灵活地强制执行类型结构。
当在 Visual Studio Code 等 IDE 中使用该函数时,这也增加了更好的开发人员体验,它将根据您提供的对象为 keys 参数创建建议。这显示在以下屏幕截图中:
了解如何在 TypeScript 中创建泛型后,您现在可以继续探索在特定情况下使用泛型。本教程将首先介绍如何在函数中使用泛型。
将泛型与函数一起使用
将泛型与函数一起使用的最常见场景之一是当您有一些代码不容易为所有用例键入时。为了使该功能适用于更多情况,您可以包括泛型类型。
在此步骤中,您将运行一个恒等函数示例来说明这一点。您还将探索一个异步示例,了解何时将类型参数直接传递给您的泛型,以及如何为您的泛型类型参数创建约束和默认值。
分配通用参数
看一下下面的函数,它返回作为第一个参数传入的内容:
function identity(value) {
return value;
}
您可以添加以下代码以使函数在 TypeScript 中类型安全:
function identity<T>(value: T): T{
return value;
}
你把你的函数变成了一个泛型函数,它接受泛型类型参数 T,这是第一个参数的类型,然后将返回类型设置为与 : T 相同。
接下来,添加以下代码来试用该功能:
function identity<T>(value: T): T {
return value;
}
const result = identity(123);
结果的类型为 123,这是您传入的确切数字。这里的 TypeScript 从调用代码本身推断泛型类型。这样调用代码不需要传递任何类型参数。您也可以显式地将泛型类型参数设置为您想要的类型:
function identity<T>(value: T): T {
return value;
}
const result = identity<number>(123);
在此代码中,result 具有类型编号。通过使用 <number> 代码传入类型,您明确地让 TypeScript 知道您希望身份函数的泛型类型参数 T 的类型为 number。这将强制将数字类型作为参数和返回值。
直接传递类型参数
直接传递类型参数在使用自定义类型时也很有用。例如,看看下面的代码:
type ProgrammingLanguage = {
name: string;
};
function identity<T>(value: T): T {
return value;
}
const result = identity<ProgrammingLanguage>({ name: "TypeScript" });
在此代码中,result 具有自定义类型 ProgrammingLanguage,因为它直接传递给标识函数。如果您没有明确包含类型参数,则结果将具有类型 { name: string } 。
使用 JavaScript 时的另一个常见示例是使用包装函数从 API 检索数据:
async function fetchApi(path: string) {
const response = await fetch(`https://example.com/api${path}`)
return response.json();
}
此异步函数将 URL 路径作为参数,使用 fetch API 向 URL 发出请求,然后返回 JSON 响应值。在这种情况下,fetchApi 函数的返回类型将是 Promise<any>,这是对 fetch 的响应对象调用 json() 的返回类型。
将 any 作为返回类型并不是很有帮助。any 表示任何 JavaScript 值,使用它你将失去静态类型检查,这是 TypeScript 的主要优点之一。如果您知道 API 将返回给定形状的对象,则可以使用泛型使此函数类型安全:
async function fetchApi<ResultType>(path: string): Promise<ResultType>{
const response = await fetch(`https://example.com/api${path}`);
return response.json();
}
突出显示的代码将您的函数转换为接受 ResultType 泛型类型参数的泛型函数。此泛型类型用于函数的返回类型:Promise<ResultType>。
注意:由于您的函数是异步的,因此,您必须返回一个 Promise 对象。TypeScript Promise 类型本身是一种通用类型,它接受 promise 解析为的值的类型。
如果仔细查看您的函数,您会发现参数列表或 TypeScript 能够推断其值的任何其他地方都没有使用泛型。这意味着调用代码在调用您的函数时必须显式传递此泛型的类型。
以下是检索用户数据的 fetchApi 通用函数的可能实现:
type User = {
name: string;
}
async function fetchApi<ResultType>(path: string): Promise<ResultType> {
const response = await fetch(`https://example.com/api${path}`);
return response.json();
}
const data = await fetchApi<User[]>('/users')
export {}
在此代码中,您将创建一个名为 User 的新类型,并使用该类型的数组 (User[]) 作为 ResultType 泛型参数的类型。数据变量现在具有类型 User[] 而不是任何。
注意:当您使用 await 异步处理函数的结果时,返回类型将是 Promise<T> 中 T 的类型,在本例中是通用类型 ResultType。
默认类型参数
像您一样创建通用的 fetchApi 函数,调用代码始终必须提供类型参数。如果调用代码不包含泛型类型,则 ResultType 将绑定为未知。以下面的实现为例:
async function fetchApi<ResultType>(path: string): Promise<ResultType> {
const response = await fetch(`https://example.com/api${path}`);
return
response.json();
}
const data = await fetchApi('/users')
console.log(data.a)
export {}
此代码尝试访问数据的理论上的属性。但由于数据类型未知,这段代码将无法访问对象的属性。
如果您不打算将特定类型添加到泛型函数的每次调用中,则可以将默认类型添加到泛型类型参数中。这可以通过在泛型类型之后添加 = DefaultType 来完成,如下所示:
async function fetchApi<ResultType= Record<string, any>>(path: string): Promise<ResultType> {
const response = await fetch(`https://example.com/api${path}`);
return response.json();
}
const data = await fetchApi('/users')
console.log(data.a)
export {}
使用此代码,您不再需要在调用 fetchApi 函数时将类型传递给 ResultType 泛型参数,因为它具有默认类型 Record<string, any>。这意味着 TypeScript 会将数据识别为具有字符串类型的键和任意类型的值的对象,从而允许您访问其属性。
类型参数约束
在某些情况下,泛型类型参数需要只允许将某些形状传递给泛型。要为您的泛型创建额外的特殊层,您可以对您的参数施加约束。
假设您有一个存储限制,您只能存储所有属性都具有字符串值的对象。为此,您可以创建一个函数,它接受任何对象并返回另一个对象,该对象具有与原始对象相同的键,但所有值都转换为字符串。这个函数将被称为 stringifyObjectKeyValues。
这个函数将是一个通用函数。这样,您就可以使生成的对象具有与原始对象相同的形状。该函数将如下所示:
function stringifyObjectKeyValues<T extends Record<string, any>>(obj: T) {
return Object.keys(obj).reduce((acc, key) => ({
...acc,
[key]: JSON.stringify(obj[key])
}), {} as { [K in keyof T]: string })
}
在此代码中,stringifyObjectKeyValues 使用 reduce 数组方法迭代原始键数组,将值字符串化并将它们添加到新数组中。
为确保调用代码始终将对象传递给您的函数,您在泛型类型 T 上使用类型约束,如以下突出显示的代码所示:
function stringifyObjectKeyValues<Textends Record<string, any>>(obj: T) {
// ...
}
extends Record<string, any> 被称为泛型类型约束,它允许您指定您的泛型类型必须可分配给 extends 关键字之后的类型。
在这种情况下,Record<string, any> 表示一个具有字符串类型的键和任意类型的值的对象。您可以让您的类型参数扩展任何有效的 TypeScript 类型。
在调用 reduce 时,reducer 函数的返回类型基于累加器的初始值。{} as { [K in keyof T]: string } 代码通过对空对象 {} 进行类型转换,将累加器初始值的类型设置为 { [K in keyof T]: string }。
type { [K in keyof T]: string } 创建一个新类型,它具有与 T 相同的键,但所有值都设置为字符串类型,这称为映射类型,本教程将在后面的部分中进一步探讨。
以下代码显示了 stringifyObjectKeyValues 函数的实现:
function stringifyObjectKeyValues<T extends Record<string, any>>(obj: T) {
return Object.keys(obj).reduce((acc, key) => ({
...acc,
[key]: JSON.stringify(obj[key])
}), {} as { [K in keyof T]: string })
}
const stringifiedValues = stringifyObjectKeyValues({ a: "1", b: 2, c: true, d: [1, 2, 3]})
变量 stringifiedValues 将具有以下类型:
{
a: string;
b: string;
c: string;
d: string;
}
这将确保返回值与函数的目的一致。
本节介绍了将泛型与函数一起使用的多种方法,包括直接分配类型参数以及为参数形状设置默认值和约束。
接下来,您将通过一些示例来了解泛型如何使接口和类适用于更多情况。
将泛型与接口、类和类型一起使用
在 TypeScript 中创建接口和类时,使用泛型类型参数来设置结果对象的形状会很有用。
例如,一个类可能具有不同类型的属性,具体取决于传递给构造函数的内容。在本节中,您将了解在类和接口中声明泛型类型参数的语法,并检查 HTTP 应用程序中的常见用例。
通用接口和类
要创建通用接口,您可以在接口名称之后添加类型参数列表:
interface MyInterface<T> {
field: T
}
这声明了一个接口,该接口具有一个属性字段,其类型由传递给 T 的类型确定。
对于类,语法几乎相同:
class MyClass<T> {
field: T
constructor(field: T) {
this.field = field
}
}
通用接口/类的一个常见用例是当您有一个字段,其类型取决于客户端代码如何使用接口/类时。
假设您有一个 HttpApplication 类,用于处理对 API 的 HTTP 请求,并且某些上下文值将传递给每个请求处理程序。这样做的一种方法是:
class HttpApplication<Context> {
context: Context
constructor(context: Context) {
this.context = context;
}
// ... implementation
get(url: string, handler: (context: Context) => Promise<void>): this {
// ... implementation
return this;
}
}
此类存储一个上下文,其类型作为 get 方法中处理函数的参数类型传入。在使用过程中,传递给 get 处理程序的参数类型将从传递给类构造函数的内容中正确推断出来。
...
const context = { someValue: true };
const app = new HttpApplication(context);
app.get('/api', async () => {
console.log(context.someValue)
});
在此实现中,TypeScript 会将 context.someValue 的类型推断为布尔值。
通用类型
现在已经了解了类和接口中泛型的一些示例,您现在可以继续创建泛型自定义类型。将泛型应用于类型的语法类似于将泛型应用于接口和类的语法。看看下面的代码:
type MyIdentityType<T> = T
此泛型类型返回作为类型参数传递的类型。假设您使用以下代码实现了这种类型:
...
type B = MyIdentityType<number>
在这种情况下,类型 B 将是类型 number。
通用类型通常用于创建辅助类型,尤其是在使用映射类型时。TypeScript 提供了许多预构建的帮助程序类型。
一个这样的例子是 Partial 类型,它采用类型 T 并返回另一个与 T 具有相同形状的类型,但它们的所有字段都设置为可选。Partial 的实现如下所示:
type Partial<T> = {
[P in keyof T]?: T[P];
};
这里的 Partial 类型接受一个类型,遍历其属性类型,然后将它们作为可选类型返回到新类型中。
注意:由于 Partial 已经内置到 TypeScript 中,因此将此代码编译到您的 TypeScript 环境中会重新声明 Partial 并引发错误。这里引用的Partial的实现只是为了说明。
要了解泛型类型有多么强大,假设您有一个对象字面量,用于存储从一家商店到您的业务分销网络中所有其他商店的运输成本。每个商店将由一个三字符代码标识,如下所示:
{
ABC: {
ABC: null,
DEF: 12,
GHI: 13,
},
DEF: {
ABC: 12,
DEF: null,
GHI: 17,
},
GHI: {
ABC: 13,
DEF: 17,
GHI: null,
},
}
该对象是表示商店位置的对象的集合。在每个商店位置中,都有表示运送到其他商店的成本的属性。例如,从 ABC 运往 DEF 的成本是 12。从一家商店到它自己的运费为空,因为根本没有运费。
为确保其他商店的位置具有一致的值,并且商店运送到自身的始终为空,您可以创建一个通用的帮助器类型:
type IfSameKeyThanParentTOtherwiseOtherType<Keys extends string, T, OtherType> = {
[K in Keys]: {
[SameThanK in K]: T;
} &
{ [OtherThanK in Exclude<Keys, K>]: OtherType };
};
IfSameKeyThanParentTOtherwiseOtherType 类型接收三个通用类型。第一个,Keys,是你想要确保你的对象拥有的所有键。在这种情况下,它是所有商店代码的联合。
T 是当嵌套对象字段具有与父对象上的键相同的键时的类型,在这种情况下,它表示运送到自身的商店位置。最后,OtherType 是 key 不同时的类型,表示一个商店发货到另一个商店。
你可以像这样使用它:
...
type Code = 'ABC' | 'DEF' | 'GHI'
const shippingCosts: IfSameKeyThanParentTOtherwiseOtherType<Code, null, number> = {
ABC: {
ABC: null,
DEF: 12,
GHI: 13,
},
DEF: {
ABC: 12,
DEF: null,
GHI: 17,
},
GHI: {
ABC: 13,
DEF: 17,
GHI: null,
},
}
此代码现在强制执行类型形状。如果您将任何键设置为无效值,TypeScript 将报错:
...
const shippingCosts: IfSameKeyThanParentTOtherwiseOtherType<Code, null, number> = {
ABC: {
ABC: 12,
DEF: 12,
GHI: 13,
},
DEF: {
ABC: 12,
DEF: null,
GHI: 17,
},
GHI: {
ABC: 13,
DEF: 17,
GHI: null,
},
}
由于 ABC 与自身之间的运费不再为空,TypeScript 将抛出以下错误:
OutputType 'number' is not assignable to type 'null'.(2322)
您现在已经尝试在接口、类和自定义帮助程序类型中使用泛型。接下来,您将进一步探讨本教程中已经多次出现的主题:使用泛型创建映射类型。
使用泛型创建映射类型
在使用 TypeScript 时,有时您需要创建一个与另一种类型具有相同形状的类型。这意味着它应该具有相同的属性,但属性的类型设置为不同的东西。对于这种情况,使用映射类型可以重用初始类型形状并减少应用程序中的重复代码。
在 TypeScript 中,这种结构被称为映射类型并依赖于泛型。在本节中,您将看到如何创建映射类型。
想象一下,您想要创建一个类型,给定另一个类型,该类型返回一个新类型,其中所有属性都设置为具有布尔值。您可以使用以下代码创建此类型:
type BooleanFields<T> = {
[K in keyof T]: boolean;
}
在这种类型中,您使用语法 [K in keyof T] 来指定新类型将具有的属性。keyof T 运算符用于返回具有 T 中所有可用属性名称的联合。然后使用 K in 语法指定新类型的属性是返回的联合类型中当前可用的所有属性 T键。
这将创建一个名为 K 的新类型,它绑定到当前属性的名称。这可用于使用语法 T[K] 访问原始类型中此属性的类型。在这种情况下,您将属性的类型设置为布尔值。
此 BooleanFields 类型的一个使用场景是创建一个选项对象。假设您有一个数据库模型,例如用户。
从数据库中获取此模型的记录时,您还将允许传递一个指定要返回哪些字段的对象。
该对象将具有与模型相同的属性,但类型设置为布尔值。在一个字段中传递 true 意味着您希望它被返回,而 false 则意味着您希望它被省略。
您可以在现有模型类型上使用 BooleanFields 泛型来返回与模型具有相同形状的新类型,但所有字段都设置为布尔类型,如以下突出显示的代码所示:
type BooleanFields<T> = {
[K in keyof T]: boolean;
};
type User = {
email: string;
name: string;
}
type UserFetchOptions = BooleanFields<User>;
在此示例中,UserFetchOptions 将与这样创建它相同:
type UserFetchOptions = {
email: boolean;
name: boolean;
}
创建映射类型时,您还可以为字段提供修饰符。一个这样的例子是 TypeScript 中可用的现有泛型类型,称为 Readonly<T>。Readonly<T> 类型返回一个新类型,其中传递类型的所有属性都设置为只读属性。这种类型的实现如下所示:
type Readonly<T> = {
readonly [K in keyof T]: T[K]
}
注意:由于 Readonly 已经内置到 TypeScript 中,因此将此代码编译到您的 TypeScript 环境中会重新声明 Readonly 并引发错误。这里引用的Readonly的实现只是为了说明的目的。
请注意修饰符 readonly,它作为前缀添加到此代码中的 [K in keyof T] 部分。
目前,可以在映射类型中使用的两个可用修饰符是 readonly 修饰符,它必须作为前缀添加到属性,以及 ? 修饰符,可以作为属性的后缀添加。这 ?修饰符将字段标记为可选。
两个修饰符都可以接收一个特殊的前缀来指定是否应该删除修饰符 (-) 或添加 (+)。如果仅提供修饰符,则假定为 +。
现在您可以使用映射类型基于您已经创建的类型形状创建新类型,您可以继续讨论泛型的最终用例:条件类型。
使用泛型创建条件类型
在本节中,您将尝试 TypeScript 中泛型的另一个有用功能:创建条件类型。首先,您将了解条件类型的基本结构。然后,您将通过创建一个条件类型来探索高级用例,该条件类型省略基于点表示法的对象类型的嵌套字段。
条件类型的基本结构
条件类型是根据某些条件具有不同结果类型的泛型类型。例如,看看下面的泛型类型 IsStringType<T>:
type IsStringType<T> = T extends string ? true : false;
在此代码中,您正在创建一个名为 IsStringType 的新泛型类型,它接收单个类型参数 T。在您的类型定义中,您使用的语法看起来像使用 JavaScript 中的三元运算符的条件表达式:T extends string ?真假。
此条件表达式正在检查类型 T 是否扩展了类型字符串。如果是,则结果类型将是完全正确的类型;否则,它将被设置为 false 类型。
注意:此条件表达式是在编译期间求值的。TypeScript 仅适用于类型,因此请确保始终将类型声明中的标识符读取为类型,而不是值。在此代码中,您使用每个布尔值的确切类型,true 和 false。
要尝试这种条件类型,请将一些类型作为其类型参数传递:
type IsStringType<T> = T extends string ? true : false;
type A = "abc";
type B = {
name: string;
};
type ResultA = IsStringType<A>;
type ResultB = IsStringType<B>;
在此代码中,您创建了两种类型,A 和 B。类型 A 是字符串文字“abc”的类型,而类型 B 是具有名为 name of type string 属性的对象的类型。
然后将这两种类型与 IsStringType 条件类型一起使用,并将结果类型存储到两个新类型 ResultA 和 ResultB 中。
如果检查 ResultA 和 ResultB 的结果类型,您会注意到 ResultA 类型设置为准确的类型 true,而 ResultB 类型设置为 false。这是正确的,因为 A 确实扩展了字符串类型而 B 没有扩展字符串类型,因为它被设置为具有字符串类型的单个名称属性的对象的类型。
条件类型的一个有用特性是它允许您使用特殊关键字 infer 在 extends 子句中推断类型信息。然后可以在条件的真实分支中使用这种新类型。此功能的一种可能用法是检索任何函数类型的返回类型。
编写以下 GetReturnType 类型来说明这一点:
type GetReturnType<T> = T extends (...args: any[]) => infer U ? U : never;
在此代码中,您将创建一个新的泛型类型,它是一个名为 GetReturnType 的条件类型。此泛型类型接受单个类型参数 T。
在类型声明本身内部,您正在检查类型 T 是否扩展了与函数签名匹配的类型,该函数签名接受可变数量的参数(包括零),然后您推断返回 该函数的类型创建一个新类型 U,可在条件的真实分支内使用。
U 的类型将绑定到传递函数的返回值的类型。如果传递的类型 T 不是函数,则代码将返回 never 类型。
使用您的类型和以下代码:
type GetReturnType<T> = T extends (...args: any[]) => infer U ? U : never;
function someFunction() {
return true;
}
type ReturnTypeOfSomeFunction = GetReturnType<typeof someFunction>;
在此代码中,您将创建一个名为 someFunction 的函数,该函数返回 true。然后使用 typeof 运算符将此函数的类型传递给 GetReturnType 泛型,并将结果类型存储在 ReturnTypeOfSomeFunction 类型中。
由于 someFunction 变量的类型是函数,因此条件类型将评估条件的真实分支。这将返回类型 U 作为结果。
类型 U 是从函数的返回类型推断出来的,在本例中是布尔值。如果检查 ReturnTypeOfSomeFunction 的类型,您会发现它已正确设置为布尔类型。
高级条件类型用例
条件类型是 TypeScript 中可用的最灵活的功能之一,允许创建一些高级实用程序类型。
在本节中,您将通过创建一个名为 NestedOmit<T, KeysToOmit> 的条件类型来探索这些用例之一。
此实用程序类型将能够省略对象中的字段,就像现有的 Omit<T, KeysToOmit> 实用程序类型一样,但也允许使用点表示法省略嵌套字段。
使用新的 NestedOmit<T, KeysToOmit> 泛型,您将能够使用以下示例中所示的类型:
type SomeType = {
a: {
b: string,
c: {
d: number;
e: string[]
},
f: number
}
g: number | string,
h: {
i: string,
j: number,
},
k: {
l: number,<F3>
}
}
type Result = NestedOmit<SomeType, "a.b" | "a.c.e" | "h.i" | "k">;
此代码声明了一个名为 SomeType 的类型,它具有嵌套属性的多级结构。使用 NestedOmit 泛型,传入类型,然后列出要省略的属性的键。
请注意如何在第二个类型参数中使用点符号来标识要省略的键。然后将结果类型存储在 Result 中。
构造此条件类型将使用 TypeScript 中可用的许多功能,例如,模板文字类型、泛型、条件类型和映射类型。
要尝试这个泛型,首先创建一个名为 NestedOmit 的泛型类型,它接受两个类型参数:
type NestedOmit<T extends Record<string, any>, KeysToOmit extends string>
第一个类型参数称为 T,它必须是可分配给 Record<string, any> 类型的类型。这将是您要从中省略属性的对象的类型。
第二个类型参数叫做KeysToOmit,必须是字符串类型。您将使用它来指定要从类型 T 中省略的键。
接下来,通过添加以下突出显示的代码来检查 KeysToOmit 是否可分配给 {infer KeyPart1}.{infer KeyPart2} 类型:
type NestedOmit<T extends Record<string, any>, KeysToOmit extends string>=
KeysToOmit extends `${infer KeyPart1}.${infer KeyPart2}`
在这里,您使用模板文字字符串类型,同时,利用条件类型推断模板文字本身内部的其他两种类型。
通过推断模板文字字符串类型的两个部分,您将字符串拆分为另外两个字符串。第一部分将分配给 KeyPart1 类型,并将包含第一个点之前的所有内容。
第二部分将分配给 KeyPart2 类型,并将包含第一个点之后的所有内容。如果您将“a.b.c”作为 KeysToOmit 传递,则最初 KeyPart1 将设置为确切的字符串类型“a”,而 KeyPart2 将设置为“b.c”。
接下来,您将添加三元运算符来定义条件的第一个真分支:
type NestedOmit<T extends Record<string, any>, KeysToOmit extends string> =
KeysToOmit extends `${infer KeyPart1}.${infer KeyPart2}`
?
KeyPart1 extends keyof T
这使用 KeyPart1 extends keyof T 来检查 KeyPart1 是否是给定类型 T 的有效属性。如果您确实有一个有效的键,请添加以下代码以使条件计算为两种类型之间的交集:
type NestedOmit<T extends Record<string, any>, KeysToOmit extends string> =
KeysToOmit extends `${infer KeyPart1}.${infer KeyPart2}`
?
KeyPart1 extends keyof T
?
Omit<T, KeyPart1>
& {
[NewKeys in KeyPart1]: NestedOmit<T[NewKeys], KeyPart2>
}
Omit<T, KeyPart1> 是一种使用 TypeScript 默认附带的 Omit 助手构建的类型。此时,KeyPart1 不是点表示法:它将包含一个字段的确切名称,该字段包含您希望从原始类型中省略的嵌套字段。因此,您可以安全地使用现有的实用程序类型。
您正在使用 Omit 删除 T[KeyPart1] 中的一些嵌套字段,为此,您必须重建 T[KeyPart1] 的类型。
为避免重建整个 T 类型,您使用 Omit 仅从 T 中删除 KeyPart1,同时保留其他字段。然后,您将在下一部分的类型中重建 T[KeyPart1]。
[KeyPart1 中的新键]:NestedOmit<T[NewKeys], KeyPart2> 是一个映射类型,其中属性是可分配给 KeyPart1 的属性,这意味着您刚刚从 KeysToOmit 中提取的部分。
这是您要删除的字段的父项。如果您通过了 a.b.c,在第一次评估您的条件时,它将是“a”中的 NewKeys。
然后将此属性的类型设置为递归调用 NestedOmit 实用程序类型的结果,但现在使用 T[NewKeys] 将此属性的类型作为第一个类型参数传递给 T,并作为第二个类型参数传递其余键以点表示法表示,在 KeyPart2 中可用。
在内部条件的 false 分支中,返回绑定到 T 的当前类型,就好像 KeyPart1 不是 T 的有效键一样:
type NestedOmit<T extends Record<string, any>, KeysToOmit extends string> =
KeysToOmit extends `${infer KeyPart1}.${infer KeyPart2}`
?
KeyPart1 extends keyof T
?
Omit<T, KeyPart1>
& {
[NewKeys in KeyPart1]: NestedOmit<T[NewKeys], KeyPart2>
}
: T
条件的这个分支意味着你试图省略一个 T 中不存在的字段。在这种情况下,没有必要再进一步了。
最后,在外部条件的 false 分支中,使用现有的 Omit 实用程序类型从 Type 中省略 KeysToOmit:
type NestedOmit<T extends Record<string, any>, KeysToOmit extends string> =
KeysToOmit extends `${infer KeyPart1}.${infer KeyPart2}`
?
KeyPart1 extends keyof T
?
Omit<T, KeyPart1>
& {
[NewKeys in KeyPart1]: NestedOmit<T[NewKeys], KeyPart2>
}
: T
: Omit<T, KeysToOmit>;
如果条件 KeysToOmit extends `{infer KeyPart1}.{infer KeyPart2}` 为假,这意味着 KeysToOmit 没有使用点符号,因此,您可以使用现有的 Omit 实用程序类型。
现在,要使用新的 NestedOmit 条件类型,请创建一个名为 NestedObject 的新类型:
type NestedObject = {
a: {
b: {
c: number;
d: number;
};
e: number;
};
f: number;
};
然后对其调用 NestedOmit 以省略 a.b.c 处可用的嵌套字段:
type Result = NestedOmit<NestedObject, "a.b.c">;
在第一次评估条件类型时,外部条件将为真,因为字符串文字类型“a.b.c”可分配给模板文字类型“{infer KeyPart1}.{infer KeyPart2}”。
在这种情况下,KeyPart1 将被推断为字符串文字类型“a”,而 KeyPart2 将被推断为字符串的剩余部分,在本例中为“b.c”。
现在将评估内部条件。这将评估为真,因为此时 KeyPart1 是 T 的键。KeyPart1 现在是“a”,而 T 确实有一个属性“a”:
type NestedObject = {
a: {
b: {
c: number;
d: number;
};
e: number;
};
f: number;
};
继续评估条件,您现在位于内部 true 分支内。这将构建一个新类型,它是其他两种类型的交集。
第一种类型是在 T 上使用 Omit 实用程序类型以省略可分配给 KeyPart1 的字段的结果,在本例中为 a 字段。第二种类型是您通过递归调用 NestedOmit 构建的新类型。
如果您进行 NestedOmit 的下一次评估,对于第一次递归调用,交集类型现在正在构建一个类型以用作 a 字段的类型。这将重新创建一个没有您需要省略的嵌套字段的字段。
在 NestedOmit 的最终评估中,第一个条件将返回 false,因为传递的字符串类型现在只是“c”。发生这种情况时,您可以使用内置助手从对象中省略该字段。
这将返回 b 字段的类型,即省略了 c 的原始类型。现在评估结束,TypeScript 返回您要使用的新类型,并省略嵌套字段。
结论
在本教程中,我们探索适用于函数、接口、类和自定义类型的泛型,以及使用了泛型来创建映射类型和条件类型。
这些都使泛型成为您在使用 TypeScript 时可以随意使用的强大工具。正确使用它们将使您免于一遍又一遍地重复代码,并使您编写的类型更加灵活。
以上就是我今天跟你分享的全部内容,希望这些内容对你有所帮助。
上一篇: 理解 TypeScript 中的泛型概念
推荐阅读
-
一起来修炼吧!探索MySQL的世界,看看你的功力如何?
-
来瞧瞧如何通过阿里云物联网平台IoT的CoAP接入,一起探索物模型的第三个层面吧!
-
你了解单片机的所有引脚吗?来一起探索一下吧!
-
来一起探索 TypeScript 里的通用类型吧
-
epoll简介及触发模式(accept、read、send)-epoll的简单介绍 epoll在LT和ET模式下的读写方式 一、epoll的接口非常简单,一共就三个函数:1. int epoll_create(int size);创建一个epoll的句柄,size用来告诉内核这个监听的数目一共有多大。这个参数不同于select中的第一个参数,给出最大监听的fd+1的值。需要注意的是,当创建好epoll句柄后,它就是会占用一个fd值,在linux下如果查看/proc/进程id/fd/,是能够看到这个fd的,所以在使用完epoll后,必须调用close关闭,否则可能导致fd被耗尽。2. int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);epoll的事件注册函数,它不同与select是在监听事件时告诉内核要监听什么类型的事件,而是在这里先注册要监听的事件类型。第一个参数是epoll_create的返回值,第二个参数表示动作,用三个宏来表示:EPOLL_CTL_ADD:注册新的fd到epfd中;EPOLL_CTL_MOD:修改已经注册的fd的监听事件;EPOLL_CTL_DEL:从epfd中删除一个fd;第三个参数是需要监听的fd,第四个参数是告诉内核需要监听什么事,struct epoll_event结构如下:struct epoll_event { __uint32_t events; /* Epoll events */ epoll_data_t data; /* User data variable */};events可以是以下几个宏的集合:EPOLLIN :表示对应的文件描述符可以读(包括对端SOCKET正常关闭); EPOLLIN事件:EPOLLIN事件则只有当对端有数据写入时才会触发,所以触发一次后需要不断读取所有数据直到读完EAGAIN为止。否则剩下的数据只有在下次对端有写入时才能一起取出来了。现在明白为什么说epoll必须要求异步socket了吧?如果同步socket,而且要求读完所有数据,那么最终就会在堵死在阻塞里。 EPOLLOUT:表示对应的文件描述符可以写; EPOLLOUT事件:EPOLLOUT事件只有在连接时触发一次,表示可写,其他时候想要触发,那要先准备好下面条件:1.某次write,写满了发送缓冲区,返回错误码为EAGAIN。2.对端读取了一些数据,又重新可写了,此时会触发EPOLLOUT。简单地说:EPOLLOUT事件只有在不可写到可写的转变时刻,才会触发一次,所以叫边缘触发,这叫法没错的!其实,如果真的想强制触发一次,也是有办法的,直接调用epoll_ctl重新设置一下event就可以了,event跟原来的设置一模一样都行(但必须包含EPOLLOUT),关键是重新设置,就会马上触发一次EPOLLOUT事件。1. 缓冲区由满变空.2.同时注册EPOLLIN | EPOLLOUT事件,也会触发一次EPOLLOUT事件这个两个也会触发EPOLLOUT事件 EPOLLPRI:表示对应的文件描述符有紧急的数据可读(这里应该表示有带外数据到来);EPOLLERR:表示对应的文件描述符发生错误;EPOLLHUP:表示对应的文件描述符被挂断;EPOLLET: 将EPOLL设为边缘触发(Edge Triggered)模式,这是相对于水平触发(Level Triggered)来说的。EPOLLONESHOT:只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个socket的话,需要再次把这个socket加入到EPOLL队列里3. int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);等待事件的产生,类似于select调用。参数events用来从内核得到事件的集合,maxevents告之内核这个events有多大,这个maxevents的值不能大于创建epoll_create时的size,参数timeout是超时时间(毫秒,0会立即返回,-1将不确定,也有说法说是永久阻塞)。该函数返回需要处理的事件数目,如返回0表示已超时。-------------------------------------------------------------------------------------------- 从man手册中,得到ET和LT的具体描述如下EPOLL事件有两种模型:Edge Triggered (ET)Level Triggered (LT)假如有这样一个例子:1. 我们已经把一个用来从管道中读取数据的文件句柄(RFD)添加到epoll描述符2. 这个时候从管道的另一端被写入了2KB的数据3. 调用epoll_wait(2),并且它会返回RFD,说明它已经准备好读取操作4. 然后我们读取了1KB的数据5. 调用epoll_wait(2)......Edge Triggered 工作模式:如果我们在第1步将RFD添加到epoll描述符的时候使用了EPOLLET标志,那么在第5步调用epoll_wait(2)之后将有可能会挂起,因为剩余的数据还存在于文件的输入缓冲区内,而且数据发出端还在等待一个针对已经发出数据的反馈信息。只有在监视的文件句柄上发生了某个事件的时候 ET 工作模式才会汇报事件。因此在第5步的时候,调用者可能会放弃等待仍在存在于文件输入缓冲区内的剩余数据。在上面的例子中,会有一个事件产生在RFD句柄上,因为在第2步执行了一个写操作,然后,事件将会在第3步被销毁。因为第4步的读取操作没有读空文件输入缓冲区内的数据,因此我们在第5步调用 epoll_wait(2)完成后,是否挂起是不确定的。epoll工作在ET模式的时候,必须使用非阻塞套接口,以避免由于一个文件句柄的阻塞读/阻塞写操作把处理多个文件描述符的任务饿死。最好以下面的方式调用ET模式的epoll接口,在后面会介绍避免可能的缺陷。 i 基于非阻塞文件句柄 ii 只有当read(2)或者write(2)返回EAGAIN时才需要挂起,等待。但这并不是说每次read时都需要循环读,直到读到产生一个EAGAIN才认为此次事件处理完成,当read返回的读到的数据长度小于请求的数据长度时,就可以确定此时缓冲中已没有数据了,也就可以认为此事读事件已处理完成。Level Triggered 工作模式相反的,以LT方式调用epoll接口的时候,它就相当于一个速度比较快的poll(2),并且无论后面的数据是否被使用,因此他们具有同样的职能。因为即使使用ET模式的epoll,在收到多个chunk的数据的时候仍然会产生多个事件。调用者可以设定EPOLLONESHOT标志,在 epoll_wait(2)收到事件后epoll会与事件关联的文件句柄从epoll描述符中禁止掉。因此当EPOLLONESHOT设定后,使用带有 EPOLL_CTL_MOD标志的epoll_ctl(2)处理文件句柄就成为调用者必须作的事情。然后详细解释ET, LT:LT(level triggered)是缺省的工作方式,并且同时支持block和no-block socket.在这种做法中,内核告诉你一个文件描述符是否就绪了,然后你可以对这个就绪的fd进行IO操作。如果你不作任何操作,内核还是会继续通知你的,所以,这种模式编程出错误可能性要小一点。传统的select/poll都是这种模型的代表.ET(edge-triggered)是高速工作方式,只支持no-block socket。在这种模式下,当描述符从未就绪变为就绪时,内核通过epoll告诉你。然后它会假设你知道文件描述符已经就绪,并且不会再为那个文件描述符发送更多的就绪通知,直到你做了某些操作导致那个文件描述符不再为就绪状态了(比如,你在发送,接收或者接收请求,或者发送接收的数据少于一定量时导致了一个EWOULDBLOCK 错误)。但是请注意,如果一直不对这个fd作IO操作(从而导致它再次变成未就绪),内核不会发送更多的通知(only once),不过在TCP协议中,ET模式的加速效用仍需要更多的benchmark确认(这句话不理解)。在许多测试中我们会看到如果没有大量的idle -connection或者dead-connection,epoll的效率并不会比select/poll高很多,但是当我们遇到大量的idle- connection(例如WAN环境中存在大量的慢速连接),就会发现epoll的效率大大高于select/poll。(未测试)另外,当使用epoll的ET模型来工作时,当产生了一个EPOLLIN事件后,读数据的时候需要考虑的是当recv返回的大小如果等于请求的大小,那么很有可能是缓冲区还有数据未读完,也意味着该次事件还没有处理完,所以还需要再次读取: 这里只是说明思路(参考《UNIX网络编程》) while(rs) {buflen = recv(activeevents[i].data.fd, buf, sizeof(buf), 0);if(buflen < 0){// 由于是非阻塞的模式,所以当errno为EAGAIN时,表示当前缓冲区已无数据可读// 在这里就当作是该次事件已处理处.if(errno == EAGAIN)break; else return; }else if(buflen == 0) { // 这里表示对端的socket已正常关闭. } if(buflen == sizeof(buf) rs = 1; // 需要再次读取 else rs = 0; } 还有,假如发送端流量大于接收端的流量(意思是epoll所在的程序读比转发的socket要快),由于是非阻塞的socket,那么send函数虽然返回,但实际缓冲区的数据并未真正发给接收端,这样不断的读和发,当缓冲区满后会产生EAGAIN错误(参考man send),同时,不理会这次请求发送的数据.所以,需要封装socket_send的函数用来处理这种情况,该函数会尽量将数据写完再返回,返回-1表示出错。在socket_send内部,当写缓冲已满(send返回-1,且errno为EAGAIN),那么会等待后再重试.这种方式并不很完美,在理论上可能会长时间的阻塞在socket_send内部,但暂没有更好的办法. ssize_t socket_send(int sockfd, const char* buffer, size_t buflen) { ssize_t tmp; size_t total = buflen; const char *p = buffer; while(1) { tmp = send(sockfd, p, total, 0); if(tmp < 0) { // 当send收到信号时,可以继续写,但这里返回-1. if(errno == EINTR) return -1; // 当socket是非阻塞时,如返回此错误,表示写缓冲队列已满, // 在这里做延时后再重试. if(errno == EAGAIN) { usleep(1000); continue; } return -1; } if((size_t)tmp == total) return buflen; total -= tmp; p += tmp; } return tmp; } 二、epoll在LT和ET模式下的读写方式 在一个非阻塞的socket上调用read/write函数, 返回EAGAIN或者EWOULDBLOCK(注: EAGAIN就是EWOULDBLOCK) 从字面上看, 意思是: * EAGAIN: 再试一次 * EWOULDBLOCK: 如果这是一个阻塞socket, 操作将被block * perror输出: Resource temporarily unavailable 总结: 这个错误表示资源暂时不够, 可能read时, 读缓冲区没有数据, 或者, write时,写缓冲区满了 。 遇到这种情况, 如果是阻塞socket, read/write就要阻塞掉。 而如果是非阻塞socket, read/write立即返回-1, 同 时errno设置为EAGAIN. 所以, 对于阻塞socket, read/write返回-1代表网络出错了. 但对于非阻塞socket, read/write返回-1不一定网络真的出错了. 可能是Resource temporarily unavailable. 这时你应该再试, 直到Resource available. 综上, 对于non-blocking的socket, 正确的读写操作为: 读: 忽略掉errno = EAGAIN的错误, 下次继续读 写: 忽略掉errno = EAGAIN的错误, 下次继续写 对于select和epoll的LT模式, 这种读写方式是没有问题的. 但对于epoll的ET模式, 这种方式还有漏洞. epoll的两种模式 LT 和 ET
-
aps是什么意思_不同的富士APS-C画幅微单区别在哪里,档次是怎么划分的?-X-A系列原本指的是富士的入门级微单,最大的特点是没有使用富士X-Trans™CMOS 传感器,目前在售的有两款,分别是XA5和XA7。 富士(FUJIFILM)X-A5/XA5 15-45套机 富士(FUJIFILM)X-A7/XA7 15-45套机 目前这两款相机都处于历史最低价附近,XA5套机2699元,XA7套机3999元。XA5就是一个标准的入门级相机,定位就是时尚小巧自拍,在2699这个价位不要对它的性能有太多的奢求。 XA7价格来到了3999元,这就很有意思了,富士把入门型的相机价格推到了4000元,并且提供了自拍翻转屏和4K30P视频录制,这样一款相机就很有性价比了。 XE3是老款的中端相机,价格和入门级的XA7是一样的,都是3999元,这两款相机如何做选择呢?XE3有着更多的按键意味着更好的操控,但屏幕不是自拍翻转屏所以这点不如XA7好用。 要注意的是XE3用的是富士独有的X-Trans™CMOS III传感器,XA7是普通的2400万像素传感器,你可以理解为X-Trans才是富士的精髓。 富士(FUJIFILM)X-E3 15-45套机 当然,买新不买旧,XA7的新功能和自拍翻转屏可能会更适合你。 XT200是富士专门针对vlog市场推出的相机,其实之前的XA7也可以拍摄vlog,但XT200是富士官方宣传中的第一款vlog相机。数码防抖+3.5mm 麦克风口+自拍翻转屏+无裁切4K30P,这些都是XT200的优势,但这款相机也是普通的2400万像素传感器,没有用富士独有的X-Trans,可能是从价格角度考虑做了阉割吧。 富士(FUJIFILM)X-T200/XT200 微单相机 Vlog相机 富士XT30是我认为富士性价比最高的微单照相机,注意我说的是照相机。理由很简单,因为从拍照角度来看XT30和XTXT3几乎没有明显差距,主要是操控差了一些、视频性能大幅削弱,但好歹也是个有着双波轮+曝光补偿波轮+快门速度波轮的相机,操控方面不会太差的。视频方面也有着超采4K 30P的规格,支持F-log输出。 可以这么说,如果你只拍照,那么XT30是富士微单中性价比最高的,视频方面XT30也不差,只不过没有专业的10bit和4K60P而已。 富士(FUJIFILM)X-T30/XT30 15-45套机 XT3和XT4得放在一起说,这两款相机其实都挺好,420 10bit 4K60P的专业视频模式基本代表了APS-C画幅的上限水平。XT4还提升了电池续航增加了五轴防抖,配上富士独特的胶片滤镜,不管是拍照还是拍视频都非常优秀。 不要觉得这两款相机贵,同价位里能做到4K60P的微单也就是M43画幅的GGHGH5S,最便宜的G9机身也要7000多,这APS-C画幅的XT3机身接近8000也算合理价格范围内。除此之外的4K60P机身只有13998的松下S5和15999的佳能R6了。 富士(FUJIFILM)X-T3/XT3 1855套机 富士(FUJIFILM)X-T4/XT4 微单相机 套机(18-55mm) B站更新4K视频投稿后有很多人想拍摄4K升格,在很长一段时间里富士XT3和XT4是最优选,毕竟兼顾视频和拍照,对焦也还算能用。 X-Pro3和X-Pro2这两款微单可以算是旁轴相机,是富士官方意义上的旗舰级相机。从用料做工操控按键角度来说的确是旗舰级别,但视频性能方面只有4K30P,价格却比XT3还贵,可能这就是旁轴情怀带来的溢价吧。 富士(FUJIFILM)X-Pro3 微单相机 机身 黑色 我在之前的文章里提过很多次,有一些相机属于如果你想买你压根不会看测评,如果你犹豫那么这款相机不适合你,为什么这么说,因为有一些比较小众的相机可能在性能上并不好,但独特的外形、操控、体积、传承赋予了它独特的定位。譬如富士X-Pro系列微单就是旁轴的电子化,理光GR传承大师的扫街理念,尼康DF的外形源自胶片时代的相机,这些相机就不是针对大多数消费者的,定位就是小众。所以我说喜欢就买,不要考虑什么性能规格。 X100系列相机是一款不可换镜头的等效35mm旁轴数码相机,从外形看就是经典的复古造型。这两款相机和X-Pro3一样,如果你喜欢那就买,别犹豫, 你在市场上找不到同类型的其他数码相机,徕卡Q是28mm,索尼RX1R系列是35mm但外形不够复古,X100系列就是独特的你没有其他选择。 那么X100F和X100V该如何选择呢?X100F的镜头很一般甚至算不上好,如果我没记错的话和初代的X100是同款镜头,X100V的镜头是全新制作的很棒,X100V的机身性能也和XTX-Pro3差不多。 富士(FUJIFILM)X100F 数码相机 旁轴 2430万像素 富士(FUJIFILM)X100V 数码相机 旁轴 2610万像素 还是那句话,这两款相机也是那种如果你喜欢那就毫不犹豫下单的类型,而且这两款相机也没有竞品。 以前不推荐富士的原因是原厂镜头太贵,现在唯卓仕给富士出了四款可以自动对焦的大光圈镜头,覆盖35到130mm的焦段,可以基本满足人像摄影爱好者的需求。拍风景的话国产很多镜头厂商都有富士卡口的手动镜头可以选择,从这个角度来说富士微单就非常值得入手了。 和友商竞品相比:
-
反传销网8月30日发布:视频区块链里的骗子,币里的韭菜,杜子建骂人了!金融大V周召说区块链!——“一小帮骗子玩一大帮小白,被割韭菜,小白还轮流被割,割的就是你!” 什么区块链,统统是骗子 作者:周召(知乎金融领域大V,毕业于上海财经大学,目前任职上海某股权投资基金合伙人) 有人问我,区块链现在这么火,到底是不是骗局? 我的回答是: 是骗局。而且我并不是说数字货币是骗局,而是说所有搞区块链的都是骗局。 -01- 区块链是一种鸡肋技术 人类社会任何技术的发明应用,本质都是为了提高社会的生产效率。而所谓区块链技术本质不过是几种早已成熟的技术的大杂烩,冗余且十分低效,除了提高了洗钱和诈骗的效率以外,对人类社会的进步毫无贡献。 真正意义上的区块链得包含三个要素:分布式系统(包括记账和存储),无法篡改的数据结构,以及共识算法,三者互为基础和因果,就像三体世界一样。看上去挺让人不明觉厉的,而经过几年的瞎折腾,稍微懂点区块链的碰了几次壁后都已经渐渐明白区块链其实并没有什么卵用,区块链技术已经名存实亡,沦为了营销工具和传销组织的画皮。 因为符合上述定义的、以比特币为代表的原教旨区块链技术,是反效率的,从经济学角度来说,不但不是一种帕累托改进,甚至还可以说是一种帕累托倒退。 原教旨区块链技术的效率十分低下,因为要遍历所有节点,只能做非常轻量级的数据应用,一旦涉及到大量的数据传输与更新,区块链就瞎了。 一方面整条链交易速度会极慢,另一方面数据库容量极速膨胀,考虑到人手一份的存储机制,区块链其实是对存储资源和能源的一种极大的浪费。 这里还没有加上为了取得所谓的共识和挖矿消耗的巨大的能源,如果说区块链技术是屎,那么这波区块链投机浪潮可谓人类历史上最大规模的搅屎运动。 区块链也验证不了任何东西。 所谓的智能合约,即不智能,也非合约。我看有人还说,如果有了智能合约,就可以跟老板签一份放区块链上,如果明年销售业绩提升30%,就加薪10%,由于区块链不能篡改,不能抵赖,所以老板必须得执行,说得有板有眼,不懂行的愣一看,好像还真是那么回事。 但仔细一想,问题就来了。首先,在区块链上如何证明你真的达到了30%业绩提升?即便真的达到老板耍赖如何执行? 也就是说,如果区块链真这么厉害,要法院和仲裁干什么。 人类社会真正的符合成本效益原则的是代理制度。之前有人说要用区块链改造注册会计师行业,我不知道他准备怎么设计,我猜想他思路大概是这样的,首先肯定搞去中心化,让所有会计师到链上来,然后一个新人要成为注册会计师就要所有会计师同意并记录在链上。 那我就请问了,我每天上班累死累活,为什么还要花时间去验证一个跟我无关的的人的专业能力?最优做法当然是组织一个委员会,让专门的人来负责,这不就是现在注册会师协会干的事儿吗?区块链的逻辑相当于什么事情都要拿出来公投,这个绝对是扯淡的。 当然这么说都有点抬举区块链了,区块链技术本身根本没有判断是非能力,如果这么高级的人工智能,靠一个无脑分布式记账就能实现的话,我们早就进入共产主义社会了。 虽然EOS等数字货币采用了超级节点,通过再中心化的方式提高效率,有点行业协会的意思,是对区块链原教旨主义的一种修正,但是依然无法突破区块链技术最本质的局限性。有人说,私有链和联盟链是区块链技术的未来,也是扯淡,因为区块链技术没有未来。如果有,说明他是包装成区块链的伪区块链技术。 区块链所涉及的所有底层技术,不管是分布式数据库技术,加密技术,还是点对点传输技术等,基本都是早已存在没什么秘密可言的技术。 比特币系统最重要的特性是封闭性和自洽性,他验证不了任何系统自身以外产生的信息的真实性。 所谓系统自身产生的信息,就是数据库数据的变动信息,有价值的基本上有且只有交易信息。所以说比特币最初不过是中本聪一种炫技的产物,来证明自己对几种技术的掌握,你看我多牛逼,设计出了一个像三体一样的系统。因此,数字货币很有可能是区块链从始至终唯一的杀手应用。 比特币和区块链概念从诞生到今天已经快10年了,很多人说区块链技术在爆发的前夜,但这个前夜好像是不是有点过长了啊朋友,跟三体里的长夜有一拼啊。都说区块链技术像是90年代初的互联网,可是90年代初的互联网在十年发展后,已经出现了一大批伟大的公司,阿里巴巴在99年都成立了,区块链怎么除了币还是币呢? 正规的数字货币未来发展的形式无外乎几种,要么就是论坛币形式,或者类似股票的权益凭证等。问题是论坛币和股票之前,本来也都电子化了,区块链来了到底改变了什么呢? 所有想把TOKEN和应用场景结合起来的人最后都很痛苦,最后他们会发现区块链技术就是脱裤子放屁,自己辛苦搞半天,干嘛不自己作为中心关心门来收钱?最后这些人都产生了价值的虚无感,最终精神崩溃,只能发币疯狂收割韭菜,一边嘴里还说着我是个好人之类的奇怪的话。 因此,之前币圈链圈还泾渭分明,互相瞧不起,但这两年链圈逐渐坐不住了,想着是不是趁着泡沫没彻底破灭之前赶快收割一波,不然可能什么都捞不着了。 前段时间和一个名校毕业的链圈朋友瞎聊天,他说他们“致力于用区块链技术解决数字版权保护问题”,我就问他一个问题,你们如何保证你链的版权所有权声明是真实的,万一盗版者抢先一步把数据放在链上怎么办。他说他们的解决方案是连入国家数字版权保护中心的数据库进行验证…… 所以说区块链技术就是个鸡肋,研究到最后都会落入效率与真实性的黑洞,很多人一头扎进链圈后才发现,真正意义上的区块链技术,其实什么都干不了。 -02- 不是蠢就是坏的区块链媒体 空气币和区块链的造富神话,让区块链自媒体也开始迎风乱扭。一群群根本不知道区块链为何物的妖魔鬼怪纷纷进驻区块链自媒体战场,开始大放厥词胡编乱造。 任何东西,但凡只要和区块,链,分,分布式,记账,加密,验证,可追溯等等这些个关键词沾到哪怕一点点,这些所谓的区块链媒体人就会像狗闻到了屎了一样疯狂地把区块链概念往上套。 这让我想起曾经一度也是热闹非凡的物联网,我曾经去看过江苏一家号称要改变世界的“物联网”企业,过去一看是生产路由器的,我黑人问号脸,对方解释说没有路由器万物怎么互联,我觉得他说得好有道理,竟无言以对。 好,下面让我们进入奇葩共赏析时间,来看看区城链媒体经常有哪些危言耸听的奇谈怪论 区块链(分布式记账)的典型应用是*?? 正如前面所说,真正意义上的区块链分布式记账,不光包括“记”这个动作,还包括分布式存储和共识机制等。而*诞生远远早于区块链这个词的出现,勉强算是“分布式编辑”吧,就被很多区块链媒体拿来强行充当区块链技术应用的典范。 其实事实恰恰相反,*恰恰是去中心化失败的典范,现在如果没有精英和专业人士的编辑和维护,*早就没法看了。 区块链会促进社会分工?? 罗振宇好像就说过类似的话,虽然罗振宇说过很多没有逻辑的话,但这句话绝对是最没逻辑思维的。很多区块链自媒体也常常用这句话来忽悠老百姓,说分工代表效率提高社会进步,而区块链“无疑”会促进分工,他们的理由仅仅是分工和分布式记账都共用一个“分”字,就强行把他们扯到一起。 实际情况恰恰相反,区块链是逆分工的,区块链精神是号召所有人积极地参与到他不擅长也不想掺合的事情里面去。 区块链不能像上帝一样许诺他的子民死后上天国,只能给他们许诺你们是六度人脉中的第一级,我可以赚后面五级人的钱,你处于金字塔的顶端。
-
如何 DIY 专属的3D打印控制台?一起来探索这篇指南吧!
-
为何要用Hash值来保存密码?一起来探索Hash算法的奥秘吧!
-
热爱开源的 RocketMQ 追随者们!一起来探索行业应用和实践经验吧!