Примитивные типы являются странным отдельным компонентом системы типов ArkTS: они
не находятся в субтипировании с другими типами (включая Object
), они не могут быть частью объединённых типов или использоваться как аргументы типа для шаблонов.
Чтобы имитировать эти возможности, ArkTS предоставляет "большие буквы" эквиваленты всем этим типам (Number
для number
и т.д.). Когда это необходимо, происходит неявное преобразование (оба направления, "упаковка" и "распаковка"), что усложняет семантический анализ программ на ArkTS. Кроме того, синтаксически можно записывать конструкции, такие как double | undefined
или Set<number>
, которые на самом деле интерпретируются как Double | undefined
и Set<Number>
соответственно; это может ввести в заблуждение программиста и приводить к непредвиденному поведению в некоторых случаях.
С точки зрения инструментов, обработка примитивных типов и вставка операций "упаковки"/"распаковки" усложняет компилятор es2panda
и приводит к постоянному потоку ошибок.
number/Number
как пример примитивного значения в остальной части текста. Я буду называть эти типы примитивно-подобными. Я использую словоформу "текущий язык", чтобы указать текущее состояние спецификации — с примитивными типами как отдельной сущностью, и "предложенный язык" для языка без примитивных типов.)Удалите понятие примитивного типа из определения языка ArkTS; приблизительно сделайте number
синонимом текущего Number
, подклассом Object
.Можно также сохранить синонимы void/Void
, undefined/Undefined
, null/Null
, never/Never
, string/String
. Для каждой пары этих типов большие и маленькие буквы будут относиться к одному и тому же типу.
Арифметические и другие операции применяются напрямую к Number
и т. д., без промежуточной распаковки. Эти типы могут участвовать в объединении типов или использоваться как аргументы типа для шаблонов.
Операторы сравнения ==
и ===
должны сравнивать эти объекты по значению, как они делают для string
.
Поскольку все эквиваленты текущих примитивных типов являются неизменяемыми, такие операции, как ++
и --
, создают новые объекты. (Точно так же, в настоящее время код, такой как
let x: Number;
x++;
эквивалентен следующему:
let x: Number;
x = new Number(x.value + 1);
) Это тот же стратегический подход, который используется в Kotlin: примитивные типы данных являются просто особым случаем объектов; факт того, что в байткоде они представлены как примитивы, является лишь оптимизацией компилятора.Некоторые фрагменты кода начинают означать одно и то же:
Применимо к примитивам | Применимо к упакованным примитивам | |
---|---|---|
let x = 1.0 |
let xx = new Number(1.0) |
|
x++ |
xx++ |
в текущем языке, упакованная версия означает xx = new Number(xx.value + 1)
|
x == 0 |
xx == 0 |
версия с Number в текущем языке означает xx.unboxed() == 0
|
x === 0 |
xx === 0 |
одинаково |
function f(xx: Number){}; f(x) |
function f(x: number){}; f(xx) |
текущий язык указывает на неявное упаковывание и распаковывание |
function g<T>(t: T){}; g(x) |
function g<T>(t: T){}; g(xx) |
в текущем языке, версия с number включает неявное упаковывание |
`let u: number | null = 0` | `let uu: Number |
g<number>(0) |
g<Number>(0) |
снова, number в текущем языке неявно заменяется на Number (и аргумент упаковывается) |
typeof(x) == "number" |
typeof(xx) == "number" |
Оба конструкта в настоящее время возвращают одинаковое значение, поэтому ничего не меняется |
В настоящий момент применимо к примитивам | Применимо к упакованным примитивам | |
--- | --- | --- |
x.toString() |
xx.toString() |
в текущем языке методы не могут применяться к примитивам |
x instanceof Object |
xx instanceof Object |
CTE в настоящее время слева. После изменения обе версии возвращают true
|
Конструкция xx.unboxed()
становится устаревшей (возвращает просто xx
), но может быть сохранена для целей совместимости.
Некоторые конструкции становятся нелегальными (см. раздел Сложности):
let x: int = 0
let y = x as long // смысл ключевого слова `as` должен быть ограничен
Object
.- использование их в позиции получателя вызова метода обычно требует упаковки (но см. ниже относительно внутренних операций);Number
, требует упаковки;Number
, требует распаковки;Number
, требует упаковки; чтение из такого поля требует распаковки;es2panda
все такие манипуляции могут либоРассмотрим пример (я использую модифицированный и оптимизированный вручную выход текущего es2panda
):
функция id<T>(x: T) { return x }
структура C<T> {
f1: число
f2: T
}
функция основная() {
предварительная переменная x: число = 1
fldai.64 0x3ff0000000000000 // 1.0
sta.64 v0 // нет коробок
нет коробок -> нет коробок (необходимо проверить контекст)
Финальный текст:
функция id<T>(x: T) { return x }
структура C<T> {
f1: число
f2: T
}
функция основная() {
предварительная переменная x: число = 1
fldai.64 0x3ff0000000000000 // 1.0
sta.64 v0 // нет коробок
``` предварительная переменная y = x + 2.0
fldai.64 0x4000000000000000 // 2.0
fadd2.64 v0 // всё ещё нет коробок
sta.64 v1
предварительная переменная z: число = id<число>(x)
lda.64 v0
вызов.acc.короткий std.core.Double.valueOf:(f64), v0, 0x0 // коробка перед передачей как аргумент с параметром
sta.obj v6
вызов.короткий ETSGLOBAL.id:(std.core.Object), v6
checkcast std.core.Double
вызов.acc.короткий std.core.Double.unboxed:(std.core.Double), v0, 0x0 // распаковка после получения из параметра
sta.64 v2 // хранение распакованного значения
// Если бы мы сохранили всё в коробках, то x и z содержали бы одинаковый объект; теперь даже если мы заключим их в коробки, они будут представлять собой различные экземпляры.
// Но нет способа проверить равенство ссылок в нашем языке, так что всё в порядке
x++
ldai 0x1
i32tof64 // нет коробок; с точки зрения определения языка,
fadd2.64 v0 // x теперь содержит другой объект
sta v0
предварительная переменная u: число | null = x
lda.64 v0
вызов.acc.короткий std.core.Double.valueOf:(f64), v0, 0x0 // коробка перед сохранением в союзе
sta.obj v3 предварительная переменная m = u!
lda.obj v3
вызов.acc.короткий std.core.Double.unboxed:(std.core.Double), v0, 0x0 // распаковка при извлечении из союза
sta.64 v4
предварительная переменная c = новый C<число>
initobj.короткий C._ctor_:(C)
sta.obj v5
c.f1 = x
lda.64 v0 // поле примитивного типа, нет коробок
stobj.64 v5, C.f1
c.f2 = x
lda.64 v0
вызов.acc.короткий std.core.Double.valueOf:(f64), v0, 0x0
stobj.obj v5, C.f2 // поле типа параметра, коробка
x.toString()
lda.64 v0
вызов.acc.короткий std.core.Double.valueOf:(f64), v0, 0x0 // требуется коробка перед вызовом метода
вызов.acc.короткий std.core.Double.toString:(std.core.Double), v0, 0x0
Как мы видим, сгенерированная в текущей версии языка основная часть кода остается такой же.
В некоторых случаях простые правила, описанные выше, приведут к немедленному распаковыванию, за которым последует упаковывание:
id(x).toString()
lda.64 v0
call.acc.short std.core.Double.valueOf:(f64), v0, 0x0 // упаковка перед передачей как аргумента общего типа
sta.obj v6
call.short ETSGLOBAL.id:(std.core.Object), v6
checkcast std.core.Double
call.acc.short std.core.Double.unboxed:(std.core.Double), v0, 0x0 // распаковка после получения из общего типа
call.acc.short std.core.Double.valueOf:(f64), v0, 0x0 // упаковка перед вызовом метода
call.acc.short std.core.Double.toString:(std.core.Double), v0, 0x0
Однако простое оконное оптимизирование сможет удалить такие избыточные операции.В других случаях значение, хранящееся как примитив, будет упаковываться несколько раз вместо одного раза. Например, следующий код
```ts
let x: number = 0;
id(x);
id(x);
будет (если оптимизатор недостаточно умён) выполнять упаковку перед каждым вызовом общего типа, создавая два упакованных объекта. В текущей версии языка можно было бы сохранить упакованный тип в переменной и использовать его повторно:
let x: number = 0;
let xx: Number = x;
id(xx);
id(xx);
```Но случаи, когда генерируемый код является некачественным, должны встречаться достаточно редко и маловероятно приводить к значительному снижению производительности. С другой стороны, в текущей версии язык позволяет программисту выбрать некачественное представление для чисел, что также приводит к избыточному упаковыванию-распаковыванию:
```ts
let xx: Number = 0;
xx++;
xx++;
Для каждого оператора увеличения текущий компилятор генерирует код для распаковывания и упаковывания. По этому предложению будет выбран оптимальный примитивный тип.
==
и ===
Эти операторы проверяют равенство ссылок для большинства типов, но проверяют равенство значений для
Они также имеют специальное значение для void
и undefined
(и это единственная ситуация, где ==
и ===
различаются).
Поскольку поведение уже имеет специальный случай для строк и больших целых чисел, новая версия не становится более сложной и странной.
typeof
Теперь текст полностью переведён и оформлен согласно правилам перевода.Оператор typeof
должен вернуть специальные значения для упакованных примитивных типов. (Также как он делает сейчас.)
|---|---|
| `typeof(33 as byte) // "number"` | `typeof(33.byteValue()) // "number"` |
| `typeof(33 as char) // "number"` | `typeof(33.charValue()) // "number"` |
| `typeof(33 as short) // "number"` | `typeof(33.shortValue()) // "number"` |
| `typeof(33) // "number"` | `typeof(33) // "number"` |
| `typeof(33 as long) // "number"` | `typeof(33.longValue()) // "number"` |
| `typeof(33.0 as float) // "number"` | `typeof(33.0.floatValue()) // "number"` |
| `typeof(33.0) // "number"` | `typeof(33.0) // "number"` |
| `typeof("33") // "string"` | `typeof("33") // "string"` |
```### Массивы
Хранение упакованных примитивов в массивах является неэффективным.
- Вариант 1. (Похожий на Kotlin) Использование упакованных значений в массивах, таких как `number[]`.
Для этого нам потребуется набор специальных библиотечных классов, соответствующих незапакованным массивам примитивных значений, таких как `Int8Array`, ... `Float64Array`. Эти классы будут служить более эффективной альтернативой массивам, встроенным в язык. Однако эти классы гораздо менее удобны для использования, поэтому большинство кода, написанного или наследованного от TypeScript, скорее всего, будет недопустимым.
- Вариант 2. (Изменение времени выполнения) Изменение поведения `starr.obj` и `ldarr.obj`. 1. Всегда создавайте массивы типа `number[]` как массивы примитивных значений.
2. Когда компилятор знает, что он работает с массивом примитивного типа, он может использовать обычные команды байткода, такие как `starr` и `ldarr`.
3. Когда компилятор не уверен, какой тип массива используется (массив примитивных значений или объектов), он использует `starr.obj` и `ldarr.obj`.
4. `starr.obj`, когда массив состоит из примитивных значений, распаковывает элемент, который требуется сохранить, и сохраняет это распакованное значение. В противном случае он действует так же, как текущий `starr.obj` (то есть работает с типами ссылочных данных).
5. Аналогично, `ldarr.obj`, когда массив состоит из примитивных значений, предоставляет доступ к элементу массива и его запаковывает, после чего запакованное значение сохраняется в аккумулятор.С этим предложением следующий код может быть сгенерирован:
```ts
function f(nn: number[], v: number): number {
let ov = nn[0];
nn[0] = v;
return ov;
}
movi v0, 0;
lda v0;
fldarr.64 a0; // специализированные версии команд байткода массивов
sta.64 v1;
lda.64 a1;
starr.64 a0, v0;
lda.64 v1;
return.64;
```функция g<T>(nn: T[], v: T): T {
let ov = nn[0];
nn[0] = v;
return ov;
}
movi v0, 0;
lda v0;
ldarr.obj a0; // объектные версии массивных байткодов
sta.obj v1; // должна работать с примитивными массивами также
lda.obj a1;
starr.obj a0, v0;
lda.obj v1;
return.obj;
пусть a0: number[] = [1, 2, 3];
f(a0, 11);
call.short ETSGLOBAL.f(f64[], f64), v0, v1; // прямой вызов, ничего не упаковывается
g(a0, 11);
lda.64 v1;
call.acc.short std.core.Double.valueOf:(f64), v0, 0x0; // скалярный аргумент упаковывается
// массивный аргумент передается как есть, модифицированные байткодные инструкции позаботятся об этом
call.short ETSGLOBAL.g(std.core.Object[], std.core.Object);
### Преобразования `as`
В текущем языке оператор `as` выполняет две различных функции в зависимости от типов, задействованных:
```typescript
let v: S;
v as T;
когда оба типа S
и T
являются ссылочными типами, v as T
просто проверяет и переинтерпретирует значение v
; новый объект не создается
когда либо S
, либо T
являются примитивными типами, v as T
выполняет преобразование, создавая новое значение (это никогда не происходит в TypeScript).Существуют два варианта решения этой проблемы:
указать специальное поведение для оператора as
для примитивных типов, чтобы сохранить совместимость с существующими кодами на ArkTS;или
[моё предпочитаемое решение, которое может быть реализовано независимо от остальной части предложения]
Как в TypeScript (или Kotlin), as
всегда выполняет проверку типа и переинтерпретацию значения. Примитивным типам потребуются специальные методы преобразования, так что, например, текущий код
3 as long
будет приводить к ошибке компиляции. Он будет выглядеть следующим образом:
3.longValue()
Кроме того, все числовые типы будут продолжать реализовывать все методы класса Numeric
:
экспортировать абстрактный класс Numeric расширяющий Объект {
публичный абстрактный байтValue(): байт;
публичный абстрактный интValue(): int;
публичный абстрактный шортValue(): short;
публичный абстрактный лонгValue(): long;
публичный абстрактный флоатValue(): float;
публичный абстрактный даблValue(): double;
}
Компилятору потребуются внутренние инструменты компиляции (panda#15204) для создания эффективного кода при вызовах этих методов. Было бы крайне приятно избавиться от всех неявных преобразований,
включая явные преобразования с помощью ключевого слова as
. На данный момент мы имеем следующие типы преобразований помимо boxing и unboxing:
Необходимо отметить, что в текущем языке перечисления обрабатываются
(почти так же, как предлагается здесь) для всех неприсваиваемых типов.
Целочисленные перечисления хранятся в регистре как базовые целочисленные значения.
Однако когда они используются как аргумент типа шаблона или компонент объединения,
каждое значение автоматически преобразуется в обертывающий класс. Например, в следующем фрагменте кода
enum E { ONE, TWO, THREE }
let e: E = E.ONE;
let ee: E | null = e;
значение e
записывается в байт-коде как целое число, но ee
содержит ссылку на синтетический класс #E
; преобразование происходит прозрачно при присвоении. ee instanceof Object
вернёт true
.
Нет способа создать объект класса #E
напрямую; этот класс невидим программисту. Спецификация говорит, что перечисления считаются либо значением, либо ссылочным типом, но она не указывает, что происходит, когда они участвуют в объединениях или служат аргументами типа. (Могу я пропустить это?)
С этим предложением перечисления должны стать подтипами Object
; их представление как примитивов будет оптимизацией, аналогичной примитивным типам.
es2panda
), члены кортежей примитивных типов могут требовать упаковки (как сейчас). Если мы перейдём к реализации кортежей через классы, то эти члены можно будет хранить в неупакованной форме.Остаточные параметры, объявленные как массивы примитивных типов, могут представляться как массивы примитивов:
function f(x: number, ...xs: number[]): number
может быть представлен как
.function f64 f(f64 a0, f64[] a1)
то же самое, как сейчас.
Это предложение не оказывает влияния на реализацию корутин и асинхронных вычислений.
Это обязательно потребует некоторых преобразований на границе языков, поэтому мы можем преобразовать предоставленный JS объект (number
или Number
) в тот формат, который ожидает байт-код ArkTS.
опять же, у нас есть возможность преобразовать каждый аргумент, похожий на примитив, в объект-"прокси" JS. Поскольку значение является неизменяемым, мы можем выбрать настоящий объект JS без какого-либо поведения прокси. На мой взгляд, лучшим вариантом будет число
.
================================= ЭПИК ==============================================
Int
до Double
) 1 ММint
ссылаться на GlobalIntType()
вместо GlobalIntegerBuiltinType()
и т.д. 0.5 ММЭтот подход позволяет более эффективно использовать примитивные типы данных, обеспечивая оптимальное использование памяти и повышая производительность. Он также улучшает совместимость с другими языками программирования, такими как JavaScript, что позволяет легко преобразовывать данные между ними.### Примеры применения
Алексей Недория
:
Юра Бронников
:Я сознательно пропускаю все вопросы, связанные с массивами.
См. следующий пункт для таких правил.
В большинстве случаев типы, аналогичные примитивам, используются в своей примитивной форме. Они упакованы:
class C<T> {
n1: number; // примитив хранится
n2: number | undefined; // упакован
n3: T; // упакованный объект хранится в поле класса C<number>
}
function f<T>(n4: number, // примитив на уровне байткода
n5: number | undefined, // упакован
n6: T) { // упакован
n4.toString(); // упакован
}
```Сложность: наследование от шаблонного класса
```ts
class B<T> {
f: T // Поле останется упакованным после наследования
f(arg: T): T
}
class D extends B<number> {
// Чтобы переопределение работало, es2panda потребуется сгенерировать мост
override f(arg: number): number {}
}
Наследование от нон-шаблонного класса, переопределение метода
class B {
f(o: number): void {}
g(): Object { return new Object }
}
class D extends B {
override f(o: Object): void {} // Требуется мост
override g(): number { return 1 } // Также требует мост
}
Перегрузки:
function f(n: Number) {}
function f(n: number) {}
```Найдено в `stdlib`: `BuiltinArray.sts:tpSpliced`:
```ts
export function toSpliced(self: boolean[], start?: number, delete?: number): boolean[] {
const len = self.length;
return toSpliced(self, asIntOrDefault(start, len), asIntOrDefault(delete, len));
}
export function toSpliced(self: boolean[], start: number, delete: number, ...items: boolean[]): boolean[] {
const len = self.length;
return toSpliced(self, start as number, delete as number, ...items);
}
Оба варианта переопределяют один более эффективный (с int
вместо number/Number
).
escompat/Global.sts
:
export function isFinite(d: number): boolean {
return Double.isFinite(d);
}
export function isFinite(d: Number): boolean {
return d.isFinite();
}
Проверка с помощью instanceof
function gf<T>(v: T): boolean {
return v instanceof Object;
}
let v = 1;
console.log(gf(v)); // false в TS, true согласно этому предложению
let vv: Number = new Number(1);
console.log(gf(vv)); // true как в TS, так и согласно этому предложению
Однако текущее поведение ArkTS совпадает с предложенным.
Умные приведения типов:
let m: number = 44;
let o: Object = m; // явное преобразование не требуется
if (o instanceof number) {
// Здесь, o имеет тип number, но, вероятнее всего, es2panda будет использовать упакованное значение
let k = o + 1;
}
(Интересный факт для Kotlin на JVM:)
fun f1(o1: Any, o2: Any) {
println("${o1 === o2} ${o1 as Int === o2 as Int}");
}
fun f2(o1: Any, o2: Any) {
println("${o1 as Int === o2 as Int} ${o1 === o2}");
}
fun main() {
f1(333, 333); // false true
f2(333, 333); // true true
f1(33, 33); // true true
}
Вход Перед тем как оставить комментарий