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

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

4 ответа

Решение

Массивы - это не только фиксированная длина: они сравнимы и являются значениями (не ссылочными или указательными).

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


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 это указатель на слайс, это ошибка времени компиляции ( спецификация: выражения слайса).

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

Пример:

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

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


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

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

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

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

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

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

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

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

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

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

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

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

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

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

type Foo struct {
    buf [64]byte
}

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

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

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

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

Другие вопросы по тегам