Подтвердить что ты не робот

Скрытие нулевых значений, понимание того, почему голанг не удается здесь

Я не понимаю, как правильно заверить, что в этом случае что-то не nil:

package main

type shower interface {
  getWater() []shower
}

type display struct {
  SubDisplay *display
}

func (d display) getWater() []shower {
  return []shower{display{}, d.SubDisplay}
}

func main() {
  // SubDisplay will be initialized with null
  s := display{}
  // water := []shower{nil}
  water := s.getWater()
  for _, x := range water {
    if x == nil {
      panic("everything ok, nil found")
    }

    //first iteration display{} is not nil and will
    //therefore work, on the second iteration
    //x is nil, and getWater panics.
    x.getWater()
  }
}

Единственное, что я нашел, чтобы проверить, действительно ли это значение nil, с помощью отражения.

Это действительно нужно поведение? Или мне не удается увидеть какую-то серьезную ошибку в моем коде?

Слушать ссылку здесь

4b9b3361

Ответ 1

Проблема здесь в том, что shower является типом interface. Типы интерфейса в Go содержат фактическое значение и его динамический тип. Подробнее об этом: Законы отражения # Представление интерфейса.

Срез вы возвращаетесь содержит 2 non- nil значения. Второе значение - это значение интерфейса, пара (значение; тип), содержащая nil значение, и тип *display. Цитирование из спецификации языка Go: Операторы сравнения:

Значения интерфейса сопоставимы. Два значения интерфейса равны, если они имеют идентичные динамические типы и одинаковые динамические значения или оба имеют значение nil.

Так что, если вы сравните это с nil, это будет false. Если вы сравните его со значением интерфейса, представляющим пару (nil;*display), оно будет true:

if x == (*display)(nil) {
    panic("everything ok, nil found")
}

Это кажется невозможным, так как вам нужно знать фактический тип интерфейса. Но обратите внимание, что вы можете использовать отражение, чтобы сказать, обнуляет ли значение интерфейса non- nil значение nil используя Value.IsNil(). Вы можете увидеть пример этого на игровой площадке Go.

Почему это реализовано таким образом?

Интерфейсы в отличие от других конкретных типов (интерфейсы non-) могут содержать значения разных конкретных типов (разных статических типов). Среда выполнения должна знать динамический или тип времени выполнения значения, хранящегося в переменной типа интерфейса.

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

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

Некоторые люди говорят, что интерфейсы Go имеют динамическую типизацию, но это вводит в заблуждение. Они имеют статическую типизацию: переменная типа интерфейса всегда имеет один и тот же статический тип, и хотя во время выполнения значение, сохраненное в переменной интерфейса, может изменить тип, это значение всегда будет удовлетворять интерфейсу.

В общем, если вы хотите указать nil для значения типа interface, используйте явное значение nil а затем вы можете проверить на равенство nil. Наиболее распространенным примером является встроенный тип error который представляет собой интерфейс с одним методом. Всякий раз, когда ошибки нет, вы явно устанавливаете или возвращаете значение nil а не значение какой-то конкретной переменной (типа интерфейса non-) (что было бы очень плохой практикой, см. Демонстрацию ниже).

В вашем примере путаница возникает из фактов, которые:

  • вы хотите иметь значение в качестве типа интерфейса (shower)
  • но значение, которое вы хотите сохранить в срезе, относится не к типу shower а к конкретному типу

Поэтому, когда вы *display тип *display в shower часть, будет создано значение интерфейса, которое представляет собой пару (значение; тип), где значение равно nil а тип равно *display. Значение внутри пары будет nil, а не само значение интерфейса. Если вы поместите значение nil в срез, тогда само значение интерфейса будет равно nil и условие x == nil будет true.

демонстрация

Посмотрите этот пример: Детская площадка

type MyErr string

func (m MyErr) Error() string {
    return "big fail"
}

func doSomething(i int) error {
    switch i {
    default:
        return nil // This is the trivial true case
    case 1:
        var p *MyErr
        return p // This will be false
    case 2:
        return (*MyErr)(nil) // Same as case 1
    case 3:
        var err error // Zero value is nil for the interface
        return err    // This will be true because err is already interface type
    case 4:
        var p *MyErr
        return error(p) // This will be false because the interface points to a
                        // nil item but is not nil itself.
    }
}

func main() {
    for i := 0; i <= 4; i++ {
        err := doSomething(i)
        fmt.Println(i, err, err == nil)
    }
}

Выход:

0 <nil> true
1 <nil> false
2 <nil> false
3 <nil> true
4 <nil> false

В случае 2 возвращается nil указатель, но сначала он преобразуется в тип интерфейса (error), поэтому создается значение интерфейса, которое содержит значение nil и тип *MyErr, поэтому значение интерфейса не равно nil.

Ответ 2

Давайте думать об интерфейсе как указатель.

Скажем, у вас есть указатель a и он ноль, указывающий ни на что.

var a *int // nil

Тогда у вас есть указатель b и он указывает на a.

var b **int
b = &a // not nil

Видишь что случилось? b указывает на указатель, который указывает на ничто. Поэтому, даже если указатель равен нулю в конце цепочки, b указывает на что-то - это не ноль.

Если вы посмотрите на память процесса, она может выглядеть так:

address | name | value
1000000 | a    | 0
2000000 | b    | 1000000

Увидеть? a указывает на адрес 0 (что означает, что это nil), а b указывает на адрес a (1000000).

То же самое относится к интерфейсам (за исключением того, что они выглядят немного по-другому в памяти).

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

Здесь вы увидите, как это работает с указателями и как оно работает с интерфейсами.