Почему в Go идут массивы?

Я понимаю разницу между массивами и срезами в Go. Но я не понимаю, почему полезно иметь массивы вообще. Почему полезно, чтобы определение типа массива указывало длину и тип элемента? Почему каждый "массив", который мы используем, является срезом?

Ответы

Ответ 1

Для массивов есть нечто большее, чем просто фиксированная длина: они сопоставимы и являются значениями (не ссылочными или указательными типами).

В определенных ситуациях существует множество преимуществ массивов перед срезами, которые вместе более чем оправдывают существование массивов (вместе со срезами). Пусть увидят их. (Я даже не считаю массивы строительными блоками срезов.)


1. Сравнение означает, что вы можете использовать массивы в качестве ключей на картах, но не срезы. Да, вы могли бы сейчас сказать, что почему бы не сделать срезы сопоставимыми, чтобы одно это не оправдывало существование обоих. Равенство не очень хорошо определено на срезах. Часто задаваемые вопросы: почему карты не допускают использование срезов в качестве ключей?

Они не реализуют равенство, потому что равенство плохо определено для таких типов; Есть несколько соображений, касающихся поверхностного и глубокого сравнения, сравнения указателя и значения, как обращаться с рекурсивными типами и т.д.

2. Массивытакже могут обеспечить более высокую безопасность во время компиляции, поскольку границы индекса можно проверять во время компиляции (длина массива должна быть равна неотрицательной константе, представляемой значением типа int):

s := make([]int, 3)
s[3] = 3 // "Only" a runtime panic: runtime error: index out of range

a := [3]int{}
a[3] = 3 // Compile-time error: invalid array index 3 (out of bounds for 3-element array)

3. Также передача или присвоение значений массива неявно создаст копию всего массива, поэтому он будет "отделен" от исходного значения. Если вы передадите срез, он все равно сделает копию, но только из заголовка среза, но значение среза (заголовок) будет указывать на тот же массив резервных копий. Это может или не может быть то, что вы хотите. Если вы хотите "отделить" срез от "оригинального", вам необходимо явно скопировать содержимое, например, с помощью встроенной функции copy() для нового среза.

a := [2]int{1, 2}
b := a
b[0] = 10 // This only affects b, a will remain {1, 2}

sa := []int{1, 2}
sb := sa
sb[0] = 10 // Affects both sb and sa

4. Кроме того, поскольку длина массива является частью типа массива, массивы разной длины являются разными типами. С одной стороны, это может быть "боль в заднице" (например, вы пишете функцию, которая принимает параметр типа [4]int, вы не можете использовать эту функцию, чтобы взять и обработать массив типа [5]int), но это также может быть преимуществом: это может быть использовано для явного указания длины массива, который ожидается. Например. Вы хотите написать функцию, которая принимает адрес IPv4, ее можно смоделировать с помощью типа [4]byte. Теперь у вас есть гарантия времени компиляции, что значение, передаваемое вашей функции, будет иметь ровно 4 байта, не больше и не меньше (что в любом случае будет неверным IPv4-адресом).

5. По сравнению с предыдущим, длина массива также может служить целям документации. Тип [4]byte правильно документирует, что IPv4 имеет 4 байта. Переменная rgb типа [3]byte сообщает, что для каждого компонента цвета есть 1 байт. В некоторых случаях он даже вынимается и доступен, документируется отдельно; например, в пакете crypto/md5: md5.Sum() возвращает значение типа [Size]byte, где md5.Size является константой 16: длина контрольной суммы MD5.

6. Они также очень полезны при планировании структуры памяти структурных типов, см. ответ JimB здесь и этот ответ более подробно и в реальном примере.

7. Кроме того, поскольку срезы являются заголовками и они (почти) всегда передаются как есть (без указателей), языковая спецификация более ограничительна в отношении указателей на срезы, чем указателей на массивы. Например, спецификация предоставляет несколько сокращений для работы с указателями на массивы, в то время как то же самое дает ошибку времени компиляции в случае срезов (поскольку редко использовать указатели на срезы, если вы все еще хотите/должны это делать, вы должны быть о том, как с этим обращаться, читайте в этом ответе).

Вот такие примеры:

  • Нарезка указателя p на массив: p[low:high] является сокращением для (*p)[low:high]. Если p является указателем на слайс, это ошибка времени компиляции (spec: выражения слайса).

  • Индексирование указателя p на массив: p[i] является сокращением для (*p)[i]. Если p является указателем на фрагмент, это ошибка времени компиляции (spec: выражения индекса).

Пример:

pa := &[2]int{1, 2}
fmt.Println(pa[1:1]) // OK
fmt.Println(pa[1])   // OK

ps := &[]int{3, 4}
println(ps[1:1]) // Error: cannot slice ps (type *[]int)
println(ps[1])   // Error: invalid operation: ps[1] (type *[]int does not support indexing)

8. Доступк (одиночным) элементам массива более эффективен, чем к элементам слайса; как и в случае срезов, среда выполнения должна проходить через неявную разыменование указателя. Также "выражения len(s) и cap(s) являются константами, если тип s является массивом или указателем на массив".

Может быть, это удивительно, но вы даже можете написать:

type IP [4]byte

const x = len(IP{}) // x will be 4

Он действителен и оценивается во время компиляции, даже если IP{} не является константным выражением, например, const i = IP{} будет ошибкой во время компиляции! После этого даже не удивительно, что работает следующее:

const x2 = len((*IP)(nil)) // x2 will also be 4

Note: When ranging over a complete array vs a complete slice, there may be no performance difference at all as obviously it may be optimized so that the pointer in the slice header is only dereferenced once. For details / example, see Array vs Slice: accessing speed.


См. связанные вопросы, где можно использовать массив/имеет больше смысла, чем фрагмент:

Зачем использовать массивы вместо слайсов?

Почему срез t Go можно использовать в качестве ключей на картах Go почти так же, как массивы можно использовать в качестве ключей?

Хеш с ключом в качестве типа массива

Как элегантно проверить равенство трех значений?

Нарезка указателя фрагмента, переданного в качестве аргумента

И это просто для любопытства: фрагментможет содержать сам себя, а массив -. (На самом деле это свойство облегчает сравнение, поскольку вам не нужно иметь дело с рекурсивными структурами данных).

Обязательно прочитайте блоги:

Go Slices: использование и внутреннее устройство

Массивы, срезы (и строки): механизм 'добавления'

Ответ 2

Массивы являются значениями, и часто полезно иметь значение вместо указателя.

Значения можно сравнивать, поэтому вы можете использовать массивы в виде клавиш карты.

Значения всегда инициализируются, поэтому вам не нужно инициализировать или make их, как вы делаете, с помощью среза.

Массивы дают вам лучший контроль над макетом памяти, где, поскольку вы не можете выделить пространство непосредственно в структуре с помощью среза, вы можете с массивом:

type Foo struct {
    buf [64]byte
}

Здесь значение Foo будет содержать значение в 64 байта, а не заголовок среза, который нужно отдельно инициализировать. Массивы также используются для создания структур, соответствующих совпадению при взаимодействии с кодом C и предотвращению ложного обмена для повышения производительности кэша.

Другим аспектом повышения производительности является то, что вы можете лучше определять макет памяти, чем с помощью срезов, поскольку локальность данных может очень сильно влиять на вычисления с интенсивной памятью. Выделение указателя может занять значительное время по сравнению с операциями, выполняемыми с данными, а копирование значений, меньших, чем линия кэша, несет очень небольшую стоимость, поэтому критически важный код часто использует только массивы по этой причине.

Ответ 3

Массивы более эффективны в экономии места. Если вы никогда не обновляете размер среза (т.е. Начинаете с предопределенного размера и никогда не проходите мимо него), на самом деле разница в производительности невелика. Но в пространстве есть дополнительные накладные расходы, поскольку срез - это просто оболочка, содержащая массив в своем ядре. Контекстно, это также улучшает ясность, поскольку это делает предполагаемое использование переменной более очевидным.

Ответ 4

Каждый массив может быть срезом, но не каждый срез может быть массивом. Если у вас есть фиксированный размер коллекции, вы можете получить небольшое улучшение производительности от использования массива. По крайней мере, вы сохраните пространство, занятое заголовком среза.