Сравнимость и упорядоченность в Go

В материале мы рассмотрим, как операторы сравнения работают в Go.

В рамках статьи частично будут рассмотрены generics, но подробности мы раскрывать не будем, так как речь о generics пойдет в следующих разделах.

Comparable

Начиная с версии 1.18 в Go появился предопределенный интерфейс comparable, который позволяет описывать сравнимые типы.

// comparable - интерфейс, реализуемый всеми сравнимыми типами (логическими значениями, числами, строками, указателями, каналами, массивами сравнимых типов, структурами, поля которых являются всеми сравнимыми типами).
type comparable interface{ comparable }

Стоит отметить, что интерфейс comparable проверяется компилятором через type system и правила сравнимости, поэтому вы не сможете реализовать его самостоятельно для своей структуры.

То есть comparable ≠ интерфейс типа с методами, как fmt.Stringer.

Он является ограничителем типа и используется в дженериках.

Нельзя создать свой объект, реализующий методы интерфейса comparable и ожидать, что это сделает структуры, реализующие его, сравнимыми с помощью операторов == и !=.

Ordered

Интерфейс ordered также имеет ряд ограничений. Его можно применять только к типам, которые поддерживают встроенное сравнение: строки и числа.

Также как и comarable, ordered ≠ интерфейс с методами, а является ограничителем типа в дженериках.

Таким образом, нельзя описать и ordered интерфейс, чтобы операторы <, >, <=, >= работали для кастомных типов.

В Go 1.21 Ordered тип переехал в стандартный пакет cmp, а в более ранних версиях использовался пакет golang.org/x/exp/constraints

package cmp

// Ordered — это ограничение (constraint), которое разрешает любой упорядоченный тип: любой тип, который поддерживает операторы <, <=, >=, >.
// Если в будущих версиях Go будут добавлены новые упорядоченные типы,
// это ограничение будет изменено, чтобы включать их.
//
// Обратите внимание, что типы с плавающей запятой могут содержать значения NaN ("не число").
// Операторы, такие как == или <, всегда будут возвращать false при
// сравнении значения NaN с любым другим значением, включая другой NaN.
// См. функцию [Compare] для корректного и согласованного способа сравнения значений NaN.
type Ordered interface {
	~int | ~int8 | ~int16 | ~int32 | ~int64 |
		~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 | ~uintptr |
		~float32 | ~float64 |
		~string
}

Что может пойти не так?

Специальные значения float

В типе данных float могут использоваться специальные значения: +Inf, -Inf, NaN, которые ломают логику сравнения.

// не работает

NaN == NaN  // false
NaN < x    // false
NaN > x    // false

// Используйте
cmp.Compare(float64(NaN), float64(NaN)) // 0

Сравнение map, slice, array

Так как типы map, slice ссылочными (что такое ссылочный тип мы рассмотрим в следующих разделах), то их нельзя сравнивать, даже если они одинаковые. Проблема заключается в том, что непонятно, что именно мы должны сравнить: элементы или структуры и ссылки этих типов.

При этом массивы, если их содержимое можно сравнивать будут являться сравнимыми.

Map

var mapOne, mapTwo map[int]string

// Map сравнивать нельзя
if mapOne == mapTwo {}

// Но сравнение на nil допустимо
if mapOne == nil {}

Slice

var sliceOne, sliceTwo []int{}
// Сравнение слайсов приведет к ошибке на этапе компиляции
if sliceOne == sliceTwo {}

// а сравнение с nil допустимо
if sliceOne == nil {}

Array

var arrayOne, arrayTwo [2]int{}
// Массивы будут работать корректно
if arrayOne == arrayTwo {
    // будет true, если содержимое массивов идентично
}

// так как массив не является ссылочным типом, то сравнение с nil приведет к ошибке. Так делать нельзя
if arrayOne == nil {}

// Для сравнения с nil нужно иницилизировать переменную как ссылку на массив
var arrayRef *[2]int{}

// теперь так можно
if arrayRef == nil {}

Как работают ссылки, что такое ссылочные типы данных мы рассмотрим в следующих разделах

Сравнение структур

Важно понимать, что структуры можно сравнивать, только если все поля внутри структуры можно сравнивать.

type A struct {
    X int
    Y []byte // slice — несравнимый тип
}

var one, two A

// Сравнение вызовет ошибку:
if one == two {}

Но при этом, если изменить код

type A struct {
    X int
    Y [2]byte // массив можно сравнивать, если его элементы сравнимы
}

var one, two A

// Теперь можно сравнивать:
if one == two {}

Сравнение интерфейсов

Что выведет следующий код?

package main

func main() {
  // напомним, тип any (interface{}) сравинвать можно, а слайсы - нельзя
	var one, two any = []int{}, []int{}

	if one == two {
	}
}

Так как мы не можем сравнивать слайсы, то запуск кода приведет к панике

panic: runtime error: comparing uncomparable type []int

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *