Общие функции на ломтиках: руководство, которое поможет вам понять

Общие функции на ломтиках: руководство, которое поможет вам понять

31 мая 2025 г.

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

СТип параметровМы можем писать функции какSlices.indexОдин раз для всех типов кусочков сопоставимых элементов:

// Index returns the index of the first occurrence of v in s,
// or -1 if not present.
func Index[S ~[]E, E comparable](s S, v E) int {
    for i := range s {
        if v == s[i] {
            return i
        }
    }
    return -1
}

Больше не нужно реализоватьIndexОпять же для каждого типа элемента.

АсрезыПакет содержит много таких помощников для выполнения общих операций на ломтиках:

    s := []string{"Bat", "Fox", "Owl", "Fox"}
    s2 := slices.Clone(s)
    slices.Sort(s2)
    fmt.Println(s2) // [Bat Fox Fox Owl]
    s2 = slices.Compact(s2)
    fmt.Println(s2)                  // [Bat Fox Owl]
    fmt.Println(slices.Equal(s, s2)) // false

Несколько новых функций (InsertВReplaceВDeleteи т. д.) изменить срез. Чтобы понять, как они работают и как правильно их использовать, нам нужно изучить основную структуру срезов.

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

Например, этот ломтикsявляется представлением о 4 элементах массива размера 6:

Если функция изменяет длину среза, пройденного в виде параметра, то она должна вернуть новый срез на вызывающего абонента. Основной массив может остаться прежним, если он не должен расти. Это объясняет почемудобавлятьиslices.Compactвернуть значение, ноslices.Sort, что просто переорчает элементы, нет.

Рассмотрим задачу удаления части среза. Перед дженериками стандартный способ удаления частиs[2:5]от срезовsдолжен был вызватьдобавлятьФункция для копирования конечной части над средней частью:

s = append(s[:2], s[5:]...)

Синтаксис был сложным и подверженным ошибкам, с участием подкладки и переменного параметра. Мы добавилиslices.deleteЧтобы упростить удаление элементов:

func Delete[S ~[]E, E any](s S, i, j int) S {
       return append(s[:i], s[j:]...)
}

Однострочная функцияDeleteБолее ясно выражает намерение программиста. Давайте рассмотрим ломтикsдлины 6 и емкость 8, содержащие указатели:

Этот вызов удаляет элементы наs[2]Вs[3]Вs[4]от срезовs:

s = slices.Delete(s, 2, 5)

Разрыв в индексах 2, 3, 4 заполняется смещением элементаs[5]слева и установить новую длину на3Полем

DeleteНе нужно выделять новый массив, так как он меняет элементы на месте. Нравитьсяappend, он возвращает новый кусочек. Много других функций вslicesпакет следуйте по этому шаблону, включаяCompactВCompactFuncВDeleteFuncВGrowВInsert, иReplaceПолем

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

    slices.Delete(s, 2, 5) // incorrect!
    // s still has the same length, but modified contents

Проблема нежелательной жизни

Перед ходом 1,22,slices.Deleteне изменил элементы между новой и оригинальной длиной среза. В то время как возвращенный срез не будет включать эти элементы, «разрыв», созданный в конце оригинала, теперь непревзойденные срезы продолжали удерживать их. Эти элементы могут содержать указатели на большие объекты (изображение 20 МБ), и сборщик мусора не будет выпускать память, связанную с этими объектами. Это привело к утечке памяти, которая может привести к значительным проблемам с производительностью.

В приведенном выше примере мы успешно удаляем указателиp2Вp3Вp4отs[2:5], сдвинув один элемент налево. Ноp3иp4все еще присутствуют в основном массиве, помимо новой длиныsПолем Коллекционер мусора не вернет их. Менее очевидно,p5не является одним из удаленных элементов, но его память все еще может протекать из -заp5Указатель хранится в серой части массива.

Это может быть запутанным для разработчиков, если бы они не знали, что «невидимые» элементы все еще используют память.

Итак, у нас было два варианта:

  • Либо сохранить эффективную реализациюDeleteПолем Позвольте пользователям установить устаревшие указатели наnilСами, если они хотят убедиться, что указанные значения могут быть освобождены.
  • Или изменениеDeleteЧтобы всегда устанавливать устаревшие элементы на ноль. Это дополнительная работа, созданиеDeleteнемного менее эффективно. Обновления указателей (устанавливая их наnil) включает в себя коллекцию мусора объектов, когда они становятся иначе недоступными.

Не было очевидно, какой вариант был лучшим. Первый обеспечил производительность по умолчанию, а второй - по умолчанию, чтобы по умолчанию дало фругаренность памяти.

Исправление

Ключевым наблюдением является то, что «установить устаревшие указатели наnil«Не так просто, как кажется.CompactВCompactFuncВDeleteВDeleteFuncВReplace«очистить хвост». Как хороший побочный эффект, когнитивная нагрузка уменьшается, и пользователям теперь не нужно беспокоиться об этих утечках памяти.

В Go 1.22, это то, на что выглядит память после вызова Delete:

Код, измененный в пяти функциях, использует новую встроенную функциюпрозрачный(GO 1.21) Чтобы установить устаревшие элементы на ноль значения типа элементаs:

Нулевое значениеEявляетсяnilкогдаEэто тип указателя, среза, карты, чана или интерфейса.

Испытания провалы

Это изменение привело к некоторым тестам, которые прошли в GO 1.21, теперь сбой в GO 1.22, когда функции срезов используются неправильно. Это хорошие новости. Когда у вас есть ошибка, тесты должны сообщить вам.

Если вы проигнорируете возвращаемое значениеDelete:

slices.Delete(s, 2, 3)  // !! INCORRECT !!

тогда вы можете неправильно предположить, чтоsне содержит никакого указателя NIL.Пример на игровой площадке GoПолем

Если вы проигнорируете возвращаемое значениеCompact:

slices.Sort(s) // correct
slices.Compact(s) // !! INCORRECT !!

тогда вы можете неправильно предположить, чтоsправильно отсортирован и уплотнен.ПримерПолем

Если вы назначите возвращаемое значениеDeleteк другой переменной и продолжайте использовать исходный ломтик:

u := slices.Delete(s, 2, 3)  // !! INCORRECT, if you keep using s !!

тогда вы можете неправильно предположить, чтоsне содержит никакого указателя NIL.ПримерПолем

Если вы случайно задерживаете переменную среза и продолжите использовать исходный ломтик:

s := slices.Delete(s, 2, 3)  // !! INCORRECT, using := instead of = !!

тогда вы можете неправильно предположить, чтоsне содержит никакого указателя NIL.ПримерПолем

Заключение

APIslicesПакет-это чистое улучшение по сравнению с традиционным синтаксисом до генериков для удаления или вставки элементов.

Мы призываем разработчиков использовать новые функции, избегая при этом «GotChas», перечисленных выше.

Благодаря недавним изменениям в реализации, класс утечек памяти автоматически избегается, без каких -либо изменений в API и без дополнительной работы для разработчиков.

Дальнейшее чтение

Подпись функций вslicesПакет сильно влияет на специфику представления срезов в памяти. Мы рекомендуем читать

  • Go ломтики: использование и внутренние
  • Массивы, ломтики: механика «добавления»
  • Адинамический массивСтруктура данных
  • Адокументациясрезов пакета

АОригинальное предложениеО обновлении устаревших элементов содержит много деталей и комментариев.


Валентин ДеЛиплс

ФотоБюшра СалкнаНеспособный

Эта статья доступна наБлог GOПод CC по лицензии 4,0.


Оригинал
PREVIOUS ARTICLE
NEXT ARTICLE