Golang: создание константного типа и ограничение значений типа

У меня вопрос о типах констант, которые ограничены определенными значениями и как вы это достигаете в Голанге. Скажем, я создаю тип unary, который имеет два постоянных значения Positive(1) и Negative(-1), и я хочу ограничить пользователя этого типа (unary) от создания других значений типа unary. Достигаю этого, создавая пакет и делая видимыми значения Positive и Negative и ограничивая тип unary содержимым пакетом? См., Например, код ниже

package unary

type unary int////not visible outside of the package unary

const (
    Positive unary = 1//visible outside of the package unary
    Negative unary = -1//visible outside of the package unary
)

func (u unary) String() string {//visible outside of the package unary
    if u == Positive {
        return "+"
    }
    return "-"
}

func (u unary) CalExpr() int {//visible outside of the package unary
    if u == Positive {
        return 1
    }
    return -1
}

Это правильный способ ограничить тип определенными константными значениями?

Ответ 1

Дефекты

Предложенное вами решение небезопасно так, как вы хотите. Можно использовать нетипизированные целочисленные константы для создания новых значений unary имеющих значение int отличное от 1 или -1. Смотрите этот пример:

p := unary.Positive
fmt.Printf("%v %d\n", p, p)

p = 3
fmt.Printf("%v %d\n", p, p)

Выход будет:

+ 1
- 3

Мы можем изменить значение p чтобы сохранить значение int 3 которое, очевидно, не равно Positive или Negative. Это возможно, потому что Spec: Assignability:

Значение x присваивается переменной типа T (" x присваивается T ") в любом из следующих случаев:

  • ...
  • x - нетипизированная константа, представимая значением типа T

3 - нетипизированная константа, и она может быть представлена значением типа unary типа, который имеет базовый тип int.

В Go вы не можете иметь "безопасные" константы, для которых "посторонние" пакеты не могут создавать новые значения, по вышеуказанной причине. Потому что, если вы хотите объявить константы в вашем пакете, вы можете использовать только выражения с "нетипизированными" версиями, которые также могут использоваться другими пакетами в присваиваниях (как в нашем примере).

Неэкспортированная структура

Если вы хотите выполнить "безопасную" часть, вы можете использовать неэкспортированные struct, но тогда они не могут быть использованы в объявлениях констант.

Пример:

type unary struct {
    val int
}

var (
    Positive = unary{1}
    Negative = unary{-1}
)

func (u unary) String() string {
    if u == Positive {
        return "+"
    }
    return "-"
}

func (u unary) CalExpr() int {
    return u.val
}

Попытка изменить его значение:

p := unary.Positive

p.val = 3 // Error: p.val undefined (cannot refer to unexported field or method val)

p = unary.unary{3} // Error: cannot refer to unexported name unary.unary
// Also error: implicit assignment of unexported field 'val' in unary.unary literal

Обратите внимание, что поскольку мы сейчас используем struct, мы можем еще больше упростить наш код, добавив string представление наших значений в struct:

type unary struct {
    val int
    str string
}

var (
    Positive = unary{1, "+"}
    Negative = unary{-1, "-"}
)

func (u unary) String() string { return u.str }

func (u unary) CalExpr() int { return u.val }

Обратите внимание, что у этого решения все еще есть "недостаток": оно использует экспортированные глобальные переменные, значения которых могут быть изменены другими пакетами. Это правда, что другие пакеты не могут создавать и назначать новые значения, но они могут делать это с существующими значениями, например:

unary.Positive = unary.Negative

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

var (
    positive = unary{1}
    negative = unary{-1}
)

func Positive() unary { return positive }

func Negative() unary { return negative }

Затем получить/использовать значения:

p := unary.Positive()

Интерфейс

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

type Unary interface {
    fmt.Stringer
    CalExpr() int
    disabler() // implementing this interface outside this package is disabled
}

var (
    Positive Unary = unary(1)  // visible outside of the package unary
    Negative Unary = unary(-1) // visible outside of the package unary
)

type unary int // not visible outside of the package unary

func (u unary) disabler() {}

func (u unary) String() string { /* ... */ }

func (u unary) CalExpr() int { /* ... */ }

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

Вот как это может выглядеть:

type MyUn struct {
    unary.Unary
}

func (m MyUn) String() string { return "/" }

func (m MyUn) CalExpr() int { return 3 }

Тестирование это:

p := unary.Positive
fmt.Printf("%v %d\n", p, p)

p = MyUn{p}
fmt.Printf("%v %d\n", p, p.CalExpr())

Выход:

+ 1
/ 3

Особый случай

Как отметил Фолькер в своем комментарии, в вашем особом случае вы могли бы просто использовать

type unary bool

const (
    Positive unary = true
    Negative unary = false
)

Поскольку тип bool имеет два возможных значения: true и false, и мы использовали все. Таким образом, нет других значений, которые можно было бы "использовать" для создания других значений нашего константного типа.

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

Также имейте в виду, что это не предотвращает такие злоупотребления, когда ожидается тип unary типа, и кто-то случайно передает нетипизированную константу, такую как true или false.

Ответ 2

Если вам нравится просто работать с int без ввода типа оболочки: классический способ сделать это в Go - это использовать открытый интерфейс с частной функцией; поэтому каждый может использовать его, но никто не может его реализовать; как:

type Unary interface {
    fmt.Stringer
    CalExpr() int
    disabler() //implementing this interface outside this package is disabled
}

var (
    Positive Unary = unary(1)  //visible outside of the package unary
    Negative Unary = unary(-1) //visible outside of the package unary
)

type unary int //not visible outside of the package unary

func (u unary) disabler() {}

func (u unary) String() string { //visible outside of the package unary
    if u == Positive {
        return "+"
    }
    return "-"
}

func (u unary) CalExpr() int { //visible outside of the package unary
    if u == Positive {
        return 1
    }
    return -1
}

Другие могут установить Positive в nil, хотя; но это не вещь в мире Го - в таких случаях.

Как упоминалось в @icza, можно перезаписать общедоступные методы. Но для частных методов Go не будет называть "самым мелким", но вместо этого вызывает оригинал.