Комментарии 58
То есть даже Go 2.0 ждать не придётся? Чудеса...
Разработчики никогда не позиционировали "Go 2.0" как мажорный релиз, в котором всё сломают и перепишут. По крайней мере, в блогах они пишут, что не стоит на это расчитывать в ближайшем будущем.
Это скорее дополнение к стандартному proposal evaluation process (который не позволяет вносить изменения, значительно меняющие язык), позволяющее, собственно, предлагать изменения вроде дженериков, которые будут значительно менять язык
Само собой.
Но, как я всегда считал, Go 2.0 может сломать Go 1.x Compatibility Promise (который, конечно, всё равно нарушают в некоторой степени)
Добавление дженериков, конечно, этого не должно сделать — это расширение языка, обратную совместимость не должно сломать.
добавлю в статью
И в этом примере я не вижу дженериков. Тоже самое можно же интерфейсами написать.
Нельзя. Если у вас есть функция, принимающая слайс интерфейсов, то слайс значений, реализующих этот интерфейс, придётся создавать самому, вручную приводя у интерфейсу, и только после этого передавать в функцию. А если написать обобщённую функцию и указать, что она принимает слайс значений некоторого типа, реализующего некий интерфейс, то этот слайс можно передать аргументом без изменений.
Для го дженерики намного менее нужны, чем в других языках. Поэтому их и добавляют только через 10 лет
Нужно понимать, что любой язык вполне живёт без дженериков. К примеру, в JS вообще нету типизации и ничего.
Отсутствие дженериков в Гоу приводит только к одному — динамической типизации в кучи моментов. А динамическая типизация в большинстве случаев — это объективный недостаток.
Но да, вполне можно программировать на динамических языках. Только зачем?
В Go не может быть динамической типизации, потому что это статически типизированный язык. Я понятия не имею, что вы имели под этим в виду, но пустой интерфейс — это скорее аналог void *
с RTTI заголовком. Вы же не назовёте C — языком с динамической типизацией из-за того что там есть void *
. Непустые интерфейсы же похожи на VMT. В С++ почему-то от наследования не отказываются из-за этого
Отсутствие дженериков в Go приводит только к тому, что люди, которые либо не писали на Go, либо переносят опыт любимых ООП языков, в интернете говорят что Go невозможно пользоваться без дженериков
Класс задач, в которых они нужны, не такой большой. Дженерики могут сделать решение некоторых задач удобнее и упростить написание библиотек, но нет никакой кучи моментов, где без них не обойтись
Ну interface
— это тот же object
из соседей, ну или any
из тайпскрипта. Т.Е. любой тип. Простите, но типизацию, где мы что-то типизируем как "любой тип" мне сложно назвать статической.
Да, если вы будете использовать в си указатель, который позволяет указывать на любой тип, я скажу, что конкретно в этом месте у вас динамическая типизация, по определению)
То, что вы не понимаете, где массово используются дженерики не означает, что они не нужны)
А на Гоу я писал. Отвратительный язык. И не только из-за дженериков. К примеру из-за уродских динамических тегов.
void* — вполне себе динамическая типизация, именно потому что это "что-то с чем-то" и вместо ошибой типов в компайл тайм мы имеем рантайм ошибка "Ожидалось что придет объект типа Х а пришел типа У". Динамическая типизация — термин очень обширный и каждый понимает свое, но обычно именно что-то в этом духе и имеется в виду
Но нет, никто не называет Java динамическим: все понимают, что Object — полиморфизм, а не динамическая типизация.
Но почему-то не все понимают, что interface{} — такой же полиморфизм, обобщённый на все типы языка, а не только на объекты.
Динамическая типизация — это когда язык сам умеет приводить значения переменных динамического типа к реальному типу. Если бы можно было написать:
a, b := interface{}(3), interface{}(7)
fmt.Println(a + b)
— это была бы динамическая типизация.Но, нет: всё, что позволяет Go — только копировать значение interface{}. Чтобы использовать значение, необходимо явное ручное приведение interface{} к его реальному типу. Никакой динамической типизации тут и близко нет.
Ещё раз. Я не говорю, что весь язык динамический, но конкретно у этой механики — динамическая природа.
это когда язык сам умеет приводить значения переменных динамического типа к реальному типу
Ну уж нет, это слабая типизация, вот даже цитата из википедии вам:
характерными атрибутами [слабой системы типов] являются понятия приведения типов
Я напомню, что Питон не приводит никакие типы автоматически, но в нём всё ещё динамическая типизация.
И да, если на Джава писать все переменные в виде object
, то это будет написание кода в динамическом стиле. Ты можешь в любой метод передать всё, что угодно, а уже локально в рантайме, а не компайл тайме будет проверяться тип.
Хочу заметить, что в том же c# использование object
считается дурным тоном и явно указывает на то, что у вас проблемы с типизацией.
class Foo {
public void Test() {}
}
/**
* Классический строго-типизированный код
*/
public class Static
{
public void Main()
{
Method(new Foo());
}
private void Method (Foo foo) {
foo.Test();
}
}
Далее — динамический код, при передаче в метод непривального аргумента, например Method(new Bar());
ошибка будет не в компайл тайме, как в статически типизированных языках, а в рантайме, как в динамически типизированных языках и да, это динамически типизированный код на C#
public class Dynamic
{
public void Main()
{
Method(new Foo());
Method(new Bar()); // we still can
}
private void Method (object foo) {
((Foo) foo).Test();
}
}
Хочу заметить, что в том же c# использование object считается дурным тоном и явно указывает на то, что у вас проблемы с типизацией.
Сомневаюсь, что в Java писать все переменные в виде object
не считается дурным тоном, и в C использование void *
везде не считается дурным тоном. В Go точно так же использование interface{}
никто не рекомендует использовать там, где это не нужно.
Нужно понимать, что пустой интерфейс — это просто вырожденный случай сущности "интерфейс", которая по определению является средством описания набора методов, которые могут быть вызваны у объекта, реализующего конкретный интерфейс.
Это не способ описания типов, это способ описания поведения.
Ну и что, скажете вы? Какая разница, если его всё равно используют как костыль, потому что нет иных инструментов?
А разница в том, что interface{}
нельзя никак использовать, потому что он не реализует никаких методов. Чтобы что-то с ним сделать, его нужно принудительно привести к статическому типу.
Если вы захотите, например, использовать std::set
в С++ для каких-то собственных классов, вам нужно будет в этот класс добавить хотя бы operator<
, несмотря на то, что std::set
является шаблонным. В Go, если вы хотите использовать "sort"
, вам точно так же нужно, чтобы объекты реализовывали sort.Interface
.
Разница с языками, у которых есть чёткие дженерики только в том, что для стандартных типов вроде int, string и т.д. такие операции нужно явно делать вручную.
В нормальных программах поведение описывается интерфейсами, которые реализуют необходимые методы, что в большинстве случаев покрывает любые требования. Исключением являются лишь т.н. "general purpose" библиотеки, в которые действительно необходимо передавать "что угодно" вроде библиотек логирования или работы с БД у которых своя система типов.
Чтобы что-то с ним сделать, его нужно принудительно привести к статическому типу.
Вот именно!
И при ошибке типа будет что?
Правильно, ошибка в рантайме.
А ошибка в рантайме при неправильном типе у нас в каких языках?
Правильно, динамических!
А нафига такую хрень вообще использовать?
Правильно, потому что нету дженериков.
Я рад, что мы с вами сошлись.
Я приведу один пример — модуль sort
, который написан авторами гоу и который должен сортировать массивы. Он позволяет отсортировать только массивы из нескольких встроенных массивов — тех, что авторы сами лично определили в библиотеке:
type IntSlice []int
type Float64Slice []float64
type StringSlice []string
То есть да, у вас есть три типа, которые вам можно использовать, а остальные — нет.
Более того, код функции Swap в этих трёх типах был просто продублирован три раза. Да, я совершенно серъёзно, они закопипастили один и тот же код три раза. Более того, чтобы расширить возможности этой библиотеки ещё на другие типы — этот код снова надо копипастить для КАЖДОГО типа, который вы хотите поддержать.
func (x IntSlice) Swap(i, j int) { x[i], x[j] = x[j], x[i] }
func (x Float64Slice) Swap(i, j int) { x[i], x[j] = x[j], x[i] }
func (x StringSlice) Swap(i, j int) { x[i], x[j] = x[j], x[i] }
В СиШарпе, к примеру, ты можешь сортировать массив из любых элементов, которые реализуют IComparable
, ну или передать отдельный IComparer<T>
— сравниватель двех элементов.
И, представьте, ничего не копипаститься для каждого нового типа.
В СиШарпе, к примеру, ты можешь сортировать массив из любых элементов, которые реализуют IComparable
который точно так же скопипащен в каждом типе:
public int CompareTo(int value) {
// Need to use compare because subtraction will wrap
// to positive for very large neg numbers, etc.
if (m_value < value) return -1;
if (m_value > value) return 1;
return 0;
}
public int CompareTo(Int64 value) {
// Need to use compare because subtraction will wrap
// to positive for very large neg numbers, etc.
if (m_value < value) return -1;
if (m_value > value) return 1;
return 0;
}
public int CompareTo(String strB) {
if (strB==null) {
return 1;
}
return CultureInfo.CurrentCulture.CompareInfo.Compare(this, strB, 0);
}
Но вы не понимаете, это другое?
Сравнение типа писать — нормально. Ведь разные типы вполне могут сравниваться одинаково. Да даже в рамках одного типа сравнение может идти по разному. Строки можно сортировать по алфавиту, по длине, по сумме кодов цифр. Обратите внимание, к сравнивателю я не имел претензий.
А вот свап двух элементов массива писать — глупо. Ведь как два элемента массива между собой поменять — совершенно не зависит от наполнения массива. Это глупый, ненужный копипаст. Разницу улавливаете?
Более того, сам по себе сравниватель должен лежать в клиентском коде, возле самих типов. Ведь типов, которые могут лежать в массиве и по которым может происходить сортировка — бесконечное множество и крайне глупо их все описывать в файлике рядом с сортом.
Сравнение типа писать — нормально
Тогда, если вы приводите контрпример в виде свопа двух элементов — то пример свопа и надо приводить.
Но мне почему-то кажется, что, к примеру, для свопа элементов в linked list и array код свопа тоже разный будет (но здесь возможно и нет, думаю если есть возможность определить оператор скобок, то и код свопа одинаков будет). Пакет sort
не делает предполжений о том, что ему передают именно массив.
Пакет sort
позволяет отсортировать вам слайсы любого типа, которые реализуют интерфейс sort.Interface
ровно точно так же, как C# позволяет отсортировать коллекции объектов, типы которых реализуют IComparable
.
сам по себе сравниватель должен лежать в клиентском коде, возле самих типов
Он там и будет лежать, потому что для клиентских типов слайсов вы его и будете определять в клиентском коде. Разница в данном случае лишь в том, что в C# вам надо для типа элемента массива определить CompareTo
, а в Go — для всего массива метод Less
.
А для стандартной библиотеки языка "клиентским кодом" будет являться сама стандартная библиотека, поэтому в ней и лежит сравниватель для типов, определённых непосредственно в самом языке.
Но мне почему-то кажется, что, к примеру, для свопа элементов в linked list и array код свопа тоже разный будет
И это тоже нормально. Вот только на примере СиШарпа для Гоу нужно было бы переопределять своп не LinkedList
и ArrayList
.
А List<Cat>
, List<Dog>
, List<Animal>
и всех остальных листов. Для каждого возможного Т в листе, который мы хотим сортировать нужно определить три метода.
А если мы хотим сортировать ArrayList<Cat>
и LinkedList<Cat>
, то для каждого из них — тоже заново написать функцию сравнения котов. Понимаете в чём разница?
Понимаете в чём разница?
В чём разница — я понимаю, но в СиШарпе вам так же нужно написать какие-то примитивы, чтобы пресловутая сортировка в контейнерах работала. Да, их будет меньше. Да, их не нужно писать для каждого контейнера. Но есть нюанс: во время работы вам нужно сортировать не "каждый возможный T", а вполне конкретный контейнер с объектами конкретного типа. Не во всяком проекте нужно сортировать много различных контейнеров, у которых различное содержимое.
Не во всяком проекте вообще нужно сортировать хоть что-нибудь. Зачастую при работе с данными из БД они уже приходят сортированными и от используемого языка это зависит примерно никак.
В таком случае, если вы не будете использовать сортировку, какое преимущество вы получаете от дженериков, если в любом языке вам не нужно будет писать дополнительный код для этого?
Да, в случае абстрактных примеров с абстрактными контейнерами любых типов дженерики ускорят процесс разработки, но обычно код для поддержки контейнеров пишется намного реже чем код для их использования, а какие-то отдельные вещи вроде сортировки тоже нужны не всегда и не настолько часто, чтобы рутина отнимала хоть сколько-то заметное время.
И, представьте, ничего не копипаститься для каждого нового типа.
В mscorlib
код Console.WriteLine
выглядит вот так:
[HostProtection(UI=true)]
[MethodImplAttribute(MethodImplOptions.NoInlining)]
public static void WriteLine(float value)
{
Out.WriteLine(value);
}
[HostProtection(UI=true)]
[MethodImplAttribute(MethodImplOptions.NoInlining)]
public static void WriteLine(int value)
{
Out.WriteLine(value);
}
[HostProtection(UI=true)]
[CLSCompliant(false)]
[MethodImplAttribute(MethodImplOptions.NoInlining)]
public static void WriteLine(uint value)
{
Out.WriteLine(value);
}
И так 15 раз. Да, я совершенно серьёзно, они закопипастили один и тот же код 15 раз. В последнем случае декораторы разные, но в остальном код почти для всех типов один и тот же.
Кроме того, чтобы WriteLine
какую-то полезную информацию выводил для своих классов, придётся переопределить ToString
, скорее всего. Но я сварщик не настоящий.
Ещё раз. Я не говорю, что весь язык динамический, но конкретно у этой механики — динамическая природа.Ещё раз: вы путаете динамику и полиморфизм. Присваивание переменной типа interface{} значения, реализующего interface{} — это именно полиморфизм, а не динамика.
Я напомню, что Питон не приводит никакие типы автоматически, но в нём всё ещё динамическая типизация.У вас странные представления о Python: 25 + True (абсолютно корректное Pytnon-выражение) — типичное для языков со слабой типизацией автоматическое преобразование типов.
Но, похоже, вы не поняли: я говорил не про автоматическое преобразование значения в значение другого типа (это и есть слабая типизация), а про возможность языка автоматически воспользоваться информацией о реальном типе значения, присвоенного переменной-интерфейсу (interface{} — лишь частный случай такого присваивания). Именно наличие этой возможности отличает динамическую типизацию от статической. И именно потому в Go динамической типизации нет.
Ещё раз: вы путаете динамику и полиморфизм
Ничего я не путаю. Способ указать "тут может быть что угодно" ещё не называется полиморфизмом, как и то, что я могу спрыгнуть с горы ещё не означает, что я умею летать, хотя в воздухе какое-то время я, конечно пробуду. То, что оно в гоу называется красивым словом "interface" ещё не означает, что это сразу стало полиморфизмом, а то, что в TypeScript называется словом any — не стало. Более того, сам по себе полиформизм ещё не означает отсутствие динамики. Полиформизм есть точно так же в полностью динамических языках.
Суть полиформизма в том, что вам не нужно уточнять тип, чтобы с ним работать. Ну, к примеру:
interface IFoo {
void DoSmth();
}
void Method (IFoo foo) {
foo.DoSmth();
}
class Bar : IFoo {};
class Qux : IFoo {};
Method(new Bar());
Method(new Qux());
Вы видите, что я передаю в метод разные типы, но продолжаю с ними работать как с одним. Это — полиморфизм. А вот использовать мета-тип "any" и потом кастить его к нужному типу и получать исключение — это динамика, а не полиморфизм. Ведь проверка и уточнение типа происходит в рантайме, а не в компайл-тайме.
к примеру
Но ведь интерфейсы в Go делают тоже самое в компайл-тайме...
type DoSmther interface {
DoSmth()
}
func Method(DoSmther foo) {
foo.DoSmth()
}
type Bar struct { }
func (*Bar) DoSmth() { }
type Qux struct { }
func (*Qux) DoSmth() {}
Method(&Bar{})
Method(&Qux{})
И конкретно это — абсолютно нормальный код. И конкретно это — полиморфизм. Вот только вы говорите сейчас про совершенно другую механику.
Мы обсуждаем необходимость использования пустого интерфейса в Гоу. В обычном коде на других языках вы врядли встретите постоянное использование object или any, а в Гоу — это в порядке вещей.
Зачем тогда вы приводите нормальный код на другом языке как контраргумент к существованию пустых интерфейсов, если этот нормальный код в Go будет использоваться точно так же?
В Go использование пустых интерфейсов — не в порядке вещей. Я специально прямо сейчас посмотрел, где в проекте, с которым я работаю, используется пустой интерфейс. Таких пакетов всего три: обёртка над protobuf для сериализации и отправки пакетов, пакет для сериализации объектов и записи их в etcd и пулер для БД, который просто аргументы передаёт в pgx. Во всех этих местах при этом явного ручного приведения типов нет нигде. Да, они внутри encoding/json
и библиотеке для protobuf есть, но это входит в класс задач, для которых, как я не раз упоминал, без интерфейсов не обойтись. Но это не "в порядке вещей", а понятные задачи.
Зачем тогда вы приводите нормальный код на другом языке как контраргумент к существованию пустых интерфейсов, если этот нормальный код в Go будет использоваться точно так же?
Этот код я привожу как объяснение того, что такое полиморфизм. А пустой интерфейс — это аналог типа object
. Вы читайте, пожалуйста, внимательнее.
Да, они внутриencoding/json
и библиотеке дляprotobuf
есть, но это входит в класс задач, для которых, как я не раз упоминал, без интерфейсов не обойтись.
В силу недостаточной выразительности языка.
Так никто и не спорит же. Да, есть такие задачи где дженерики сделали бы работу более удобной и безопасной, чем сейчас. Вот конкретно эти вещи могли бы в принципе и получить преимущества (правда в том виде, в котором они предложены в proposal я не очень понимаю как encoding/json их может использовать). Но никакой всеобъемлющей проблемы отсутствие дженериков в Go не несёт и пустыми интерфейсами код обычных приложений не изобилует, вопреки распространённому мнению.
Ещё как нужны. Есть слайсы, мапы и каналы, и они как раз таки обобщённые.
- Proposal только стартовали, а никак не принят
- Будет только в «future version of Go»
В актуальном варианте от контрактов отказались в пользу интерфейсов:
go.googlesource.com/proposal/+/refs/heads/master/design/go2draft-type-parameters.md
Наконец Го становится более-менее юзабельным?
Дженерики нужны далеко не всем. Так что, для многих, он уже давно юзабельный)
Он нужен для наверное почти всех библиотек которые сейчас подключены у меня в проекте. Ничего серхъестественного: логгеры, ормки, диай,…
Библиотеки тоже нужны не всем?
Библиотеки тоже нужны не всем?
Я об этом не говорил. И даже не делал такого вывода, разумеется.
Я имею ввиду, что не все пишут библиотеки общего назначения. Кто-то пользуется библиотеками и не очень переживает, есть внутри дженерики или нет. И вполне себе пишут много разного бизнес-код и прочего.
Я не против дженериков. Я против вывода, что без них язык "недоязык"
Я имею ввиду, что не все пишут библиотеки общего назначения. Кто-то пользуется библиотеками и не очень переживает, есть внутри дженерики или нет.
Но пользуются-то все. И вот как раз для пользователя есть огромная разница в удобстве работы. Можно просто посмотреть на сишарп версии 1.1 и 2.0, тоже без и с генериками.
Я не против дженериков. Я против вывода, что без них язык "недоязык"
Ну, я таких утверждений конечно не делаю, но мне определенно трудно представить продуктивную разработку без них. Пробовал — не получилось. Если в динамике ещё можно как-то накостылить манкипатчингом объектов в рантайме и прочей грязью, то тут и этого не было
Interface types used as type constraints can have a list of predeclared types; only type arguments that match one of those types satisfy the constraint.
То есть всё-таки решили накостылить. Не будет унификации между встроенными и пользовательскими типами.
На уровне cтатического анализа они есть https://psalm.dev/docs/annotating_code/templated_annotations/
P.S. Существующие в языке типажи (trait) очень плохо сочетаются с типизацией.
В языке Go наконец-то появятся дженерики. Proposal принят