Как стать автором
Обновить

«Что нового в Swift 2?» на примерах

Время на прочтение 16 мин
Количество просмотров 37K


Swift 2 сфокусировался на улучшении самого языка, взаимодействия с Objective-C и повышении производительности компилируемых приложений. Новые возможности Swift 2 представлены в 6 различных областях:

  • фундаментальные конструкции языка, такие, как enum , scoping (область действия), синтаксис аргументов и т.д.
  • сопоставление с образцом (pattern matching)
  • проверка доступности (availability checking)
  • расширения (extensions) протокола
  • управление ошибками (error handling)
  • взаимодействие с Objective-C

Я буду рассматривать новые возможности Swift 2, сопровождая их примерами, код которых находится на Github.

1. Фундаментальные конструкции языка


Больше нет println ( )


Обычно мы использовали функцию println( ) для печати сообщения на консоли. В версии Swift 2 мы будем использовать только print( ). Apple скомбинировала обе функции println( ) и print( ) в одну. Функция print( ), по умолчанию, печатает ваше сообщение с символом "\n" новой строки. Если вы хотите, можно вывести строку без символа новой строки:



map, filter и компания


Определение этих удобных функций и методов через коллекции в Swift 1.2 было не вполне согласованным. В Swift 1.2 не было реализации метода map по умолчанию для протокола CollectionType, так как умолчательная реализация в протокола была невозможна и расширения (extensions) были сделаны только для классов. Частично поэтому, map была определена как метод в классе Array (который реализует протокол CollectionType), и не определена в классе Set (который тоже реализует протокол CollectionType). В дополнение к этому, была задекламирована глобальная функция map, которая принимала экземпляр CollectionType в качестве первого параметра. Это создавало полную путаницу.

// Swift 1.2
     let a: [Int] = [1,2,3]
 
// здесь мы используем map как метод Array
     let b = a.map{ $0 + 1 }
 
// здесь мы вызываем глобально определенную map функцию
     map([1,2,3]) { $0 + 1 }
 
let set = Set([1,2,3])
 
// не работает, так как нет map метода для Set
     set.map{ $0 + 1 }
// глобальная функция map работает на Set
     map(set) { $0 + 1 }

Получалось, что в зависимости от типа коллекции, используется либо глобальная функция map, либо метод map этой коллекции. Выглядит несогласованным и плохо читаемым, если используется цепочка преобразований с использованием методов и функций map, filter и reduce.

Теперь в Swift 2 разрешены расширения (extensions) протокола, поэтому map, filter & co реализуются на уровне протокола для CollectionType, как расширения протокола. Следовательно, одни и те же методы будут оперировать над Array, Set или любой другой коллекцией, реализующей протокол CollectionType.

// Swift 2
 
let a: [Int] = [1,2,3]
let b = a.map{ $0 + 1 }
 
let set = Set([1,2,3])
let anotherSet = set.map{ $0 + 1 }
 
let sum = (1...100)
    .filter { $0 % 2 != 0 }
    .map    { $0 * 2 }
    .reduce(0) { $0 + $1 }
 
print(sum)
// prints out 5000

Мы видим в приведенном выше примере, что filter теперь работает на Range. В предыдущей версии это не работало, потому что, хотя Range подтверждал протокол CollectionType, но метод filter не был реализован. Теперь везде у нас намного более понятный синтаксис этих методов для любой коллекции.

Перечисления enum


В Swift 2, enum обладают достаточной reflection информацией, чтобы сделать возможной их печать.



Предложение print (an) теперь будет корректно печатать Dragon, хотя в предыдущей версии Swift вывод был совершенно не информативным (Enum Value).

Другое улучшение, касающееся enum состоит в том, что теперь Swift позволяет представлять ассоциированные значения различных типов в enum. В качестве примера, теперь можно законным образом представить тип Either:



Теперь enum могут быть рекурсивными, то есть мы можем построить дерево с помощью enum. Давайте посмотрим на этот пример:



Мы должны использовать ключевое слово indirect впереди case Node. И это позволило нам создать дерево:



Вот как оно выглядит:



Теперь мы можем создать функцию, которая рекурсивно обходит все дерево и складывает числа:



Должен быть напечатан результат, равный 21.

Диагностика.


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

Одно из простейших изменений делает код более читабельным. Как вы знаете, Swift разработчики предпочитают декларировать многие вещи как константы, используя let, а не как переменные, используя var. Но вдруг вы случайно использовали ключевое слово var? Или вы подумали, что вам ее нужно изменить, но не сделали этого? Как Xcode 7, так и Swift 2, дадут вам предупреждение, что в своем коде вы нигде эту переменную не изменяете – Xcode буквально исследует все способы использования переменной и точно знает, изменяли вы ее или нет.

Множества опций (Option Sets)


Множества опций — это способ представления множества булевских значений, и в Swift 1.x это выглядело так:

viewAnimationOptions = nil
viewAnimationOptions = .Repeat | .CurveEaseIn | .TransitionCurlUp
if viewAnimationOptions & .TransitionCurlUp != nil { ...

Этот тип синтаксиса широко использовался в Cocoa, но в действительности, это лишь «пережиток» языка C. Так что в Swift 2 он удален и представлен собственный тип для множества опций, это протокол OptionSetType:



Так что теперь множество опций может быть любым типом Set или struct, подтверждающим OptionSetType протокол. Это приводит к более понятному синтаксису при использовании множества опций:



Синтаксис не полагается на «битовые» операции, как в предыдущих версиях, и не использует nil для представления пустого множества опций.

Следует заметить, что множество опций OptionSetType теперь полагается на другую возможность в Swift 2, называемую «умолчательной» реализацией расширений протокола (default implementations for protocol extensions), так что просто подтверждая протокол OptionSetType, вы получаете реализацию по умолчанию, например, для метода contains, subtractInPlace, unionInPlace и других операций над множествами. Мы рассмотрим расширения протокола позже.

Функции и методы


Синтаксис Swift 1.x для декларирования функций и методов был унаследован от двух различных соглашений, происходяших соотетственно от C, где аргументы функции не имеют меток, и Objective-C, который снабжает аргументы методов метками. Так что у вас были такие декларации:

func save(name: String, encrypt: Bool) { ... }
class Widget {
  func save(name: String, encrypt: Bool) { ... }
  
save("thing", false)
widget.save("thing", encrypt: false)

В Swift 2, вышеприведенный код будет выглядеть так:



Так что функции получили то же самое соглашение, что и методы:

  • подразумевается, что имя первого аргумента содержится в имени функции;
  • последующие аргументы имеют метки.

Однако эти изменения не относятся к функциям, импортированным из C и Objective-C APIs.

Дополнительно, модель декларирования меток параметров стала более удобной, так как удалена опция #option, которая использовалась в Swift 1.x для обозначения параметра с одинаковым внутренним (internal) и внешним (external) именем.

Операторы области видимости ( Scoping Operators)


Новое предложение do позволяет разработчикам явно определять область видимости (explicit scope) переменных и констант. Это может быть полезно для повторного использования уже задекларированных имен или для раннего освобождения некоторых ресурсов. Предложение do выглядит так:



Для того, чтобы избежать неоднозначности с предложением do … while, которое представлено в ранних версиях Swift 1.х, в Swift 2 последний был переименован в repeat … while.

UNIT- тестирование


Проблема с unit-тестирование кода на Swift 1.x заключается в том, что Swift 1.x заставлял вас помечать словом public все то, что вы хотите, чтобы unit-тестирование видело. В результате остаются пометки public там, где они не должны быть. Все это связано с тем, что Test Target отличается от Application Target, и файлы из вашего приложения, которые являются internal, не доступны для Test Target.

В Swift 2 достигнуто существенное облегчение в unit-тестирования. Xcode 7 автоматически компилирует Swift 2 код в специальном «тестируемом» режиме



чтобы получить доступ ко всем internal определениям словно они определены как public. Это делается с помощью @testable атрибута при импорте нашего модуля.



Это все, что требуется, и вам ничего не нужно метить словом public.

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

2. Управление порядком вычислений


В Swift 2 введены новые концепции управления порядком вычислений, а также усовершенствованы уже существующие конструкции.

Предложение guard


Предложение guard, также как и предложение if, выполняет код в зависимости булевского значения условного выражения. Вы используете предложение guard для того, чтобы в случае, если булевское значение равно true, продолжить выполнение кода, следующего за предложением guard.

Предложение guard, по существу, является инверсией предложения if. Для if мы пишем:

if condition {
       // true ветка
   } else {
       // false ветка
   }

Для guard, true ветка поднимается на более высокий уровень по сравнению с false веткой:

guard condition else {
        // false ветка
    }
    // true ветка

Заметьте, что false ветка должна закончить выполнение в закрытом контексте (scope), возвращая значение или «выбрасывая» (throw) ошибку. Вы гарантируете, что код в true ветке будет выполняться только, если условие выполняется.

Это делает guard естественным способом проверки нефатальных предварительных условий без использования «пирамиды сметри», образованной вложенными if предложениями и без инверсии условий.

Давайте посмотрим, как выглядит типичный путь выполнения кода при использовании традиционного предложения if.



На вход функции createPersonFromJSON подается словарь jsonDict, а на выходе создается правильный экземпляр структуры Person, если в словаре представлена соответствующая информация, в противном случае возвращается nil. Функция написана так, как она бы выглядела в Swift 1.2 — с использованием конструкции if let . Есть пара «болевых точек» в этом коде. Во-первых, «выключение» направления правильных вычислений из основного кода, то есть, «удачное» (с точки зрения условия) направление вычислений оказалось «вложенным» в предложение if let . Во-вторых, функция createPersonFromJSON не всегда возвращает экземпляр Person, когда он нам нужен. Структура Person содержит 3 свойства, одно из которых Optional, но функция возвращает правильный экземпляр Person только, если мы получаем отличные от nil значения из словаря для всех 3-х ключей. Давайте перепишем эту функцию так: чтобы мы могли вернуть экземпляр Person, если адрес отсутствует, то есть если ключ address возвращает nil.



Мы сделали небольшое усовершенствование функциональности. Эта версия createPersonFromJSON2 функции теперь может создавать экземпляр Person даже если у адреса значение nil. Это лучше отражает структуру Person, но теперь у нас множество предложений if, а также необходимость разворачивать финальные значения, присвоенные name и age. Давайте посмотрим, как это можно усовершенствовать с новым предложением guard.



В случае guard предложения, также, как и с if let предложением, мы можем проверять присутствие значений, «разворачивать» их и присваивать константам. Однако с конструкцией guard let выполнение кода продолжается после фигурных скобок { }, если условное выражение оценивается как true. Это означает, что мы можем создать экземпляр Person внутри нормальной области действия функции без использования дополнительного ветвление кода для разворачивания значений. Если любое из значений name или age равно nil, то выполнение кода «перепрыгивает» на предложение else и осуществляется ранний возврат nil.

Давайте подведем краткие итоги в отношении guard.

  • Если условие guard предложения выполняется, выполнение кода продолжается после закрытия фигурных скобок предложения guard ;
  • Если это условие не выполняется, то выполняется код в else «ветке»; в отличие от if, guard всегда имеет >else блок;
  • Предложение else должно передавать управление за пределы нормальной области видимости функции с использованием return, break, continue> или путем вызова другой функции или метода
.

Предложение defer



Предложение defer напоминает finally в других языках программирования, за исключением того, что оно не привязано к предложению try, и вы можете его использовать где угодно. Вы пишите defer {...} и где-нибудь в коде и этот блок будет выполнен, когда управление вычислением покинет эту область видимости кода (enclosing scope), причем не имеет значения, добирается ли код до конца или получает предложение return или «выбрасывает» ошибку. Оператор defer прекрасно сочетается с guard и с обработкой ошибок (рассматривается позже).

guard let file1 = Open(...) else {
        // обрабатываем ошибки file1
        return
    }
    defer { file1.close() }

    guard let file2 = Open(...) else {
        // обрабатываем ошибки file2
        return
    }
    defer { file2.close() }

    // используем file1 и file2 
    . . . . . . .
    // нет необходимости закрывать файлы в конце, все уже сделано

Заметим, что defer работает для file1 как при нормальном течении вычислительного процесса, так и в случае ошибки с файлом file2. Это убирает из кода многочисленные повторы и помогает вам не забыть что-то «очистить» в какой-нибудь ветке вычислений. При обработке ошибок стоит та же проблема и предложение defer подходит для этих целей наилучшим образом.

Repeat — while


Swift 2.0 внес синтаксические изменения в предложение do-while, которое использовалось прежде. Вместо do-while, теперь мы получим repeat- while.



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

Когда вы используете do — while цикл, сразу же неясно, что это конструкция для повторения. Это особенно справедливо, если блок кода внутри do предложения большой, и условие while находится за пределами экрана. Для смягчения этого обстоятельства, ключевое слово do заменено на repeat, которое проясняет для пользователя, что это повторяющийся блок кода.

Ключевое слово do имеет новое назначение в Swift 2 в новой модели обработки ошибок, исследованием которой мы займемся позже.

Pattern matching


Swift всегда имел мощные возможности pattern matching (соответствие по образцу), но только в конструкции switch. Конструкция switch рассматривала значение value и сравнивала его с несколькими возможными образцами. Одним из недостатков предложения switch является то, что мы должны представить все возможные варианты значения value, то есть оператор switch должен быть исчерпывающим (exhaustive) и это вызывает неудобство использования. Поэтому Swift 2 портировал возможности pattern matching, которые прежде были только у switch / case, другим предложениям, управляющим потоком вычислений. if case — это один из них, и он позволяет переписать код с switch более кратко. Другими предложениями являются for case и while case.

Pattern matching if case


Новым в Swift 2является поддержка pattern matching внутри предложений ifguard). Давайте сначала определим простейшее перечисление Number, а затем покажем способы его применения.



1. Проверка определенного варианта (case)


Используем case: мы хотим проверить, соответствует ли значение определенному case. Это работает несмотря на то, имеет ли этот case ассоциированное значение или нет, но значение не восстанавливается (если оно существует).



Образец начинается с case .IntegerValue, а значение, которое должно соответствовать этому образцу, переменная myNumber, идет после знака = равенства. Это может показаться нелогичным, но то же самое мы видим при «развертывании» Optional значения a1 в конструкции if let a = a1: значение a1, которое проверяется, идет после знака равенства.

Вот эквивалентная Swift 1.2 версия, использующая switch:



2. Получение ассоциированного значения


Используем case: мы хотим проверить, соответствует ли значение определенному case, а также извлечь ассоциированное значение (или значения).



«Образец» теперь превратился в case let .IntegerValue(theInt). Значение, которое должно соответствовать «образцу» то же, что и в предыдущем примере.

Ниже приведен пример, отражающий ту же самую концепцию, но применительно к guard. Семантика предикатов для guard и if идентичная, так что pattern matching работает точно также.



3. Отбор с помощью предложения where


К любому case в предложении guard может быть добавлено (необязательно) предложение where для обеспечения дополнительных ограничений. Давайте модифицируем функцию getObjectInArray:atIndex: из предыдущего примера:



4. Соответствие диапазону range




5. Используем кортеж tuple




6. Сложные if предикаты


Предложение if в Swift 2 оказалось на удивление способным. Оно может иметь множество предикатов, разделенных запятой. Предикаты попадают в одну из трех категорий:

  • Простейшие логические тесты (например, foo == 10 || bar > baz). Может быть только один такой предикат и он должен помещаться на первом месте.
  • Разворачивание Optional (например, let foo = maybeFoo where foo > 10). Если за предикатом разворачивания Optional сразу же следует другой предикат разворачивания Optional, то let можно пропустить. Можно дополнить квалификатором where.
  • Pattern matching (например, case let .Bar(something) = theValue), — это то, что мы рассматривали выше. Можно дополнить квалификатором where.

Предикаты оцениваются в порядке их определения и после не выполнения какого-то предиката, остальные не оцениваются.

Pattern matching for case


Pattern matching может использоваться в содружестве с циклом for -in. В этом случае наши намерения состоят в том, чтобы пройтись по элементам последовательности, но только по тем, которые соответствуют заданному «образцу». Вот пример:



Заметим, что также, как «образцы» в предложении switch, вы можете извлекать множество ассоциированных значений и использовать _, если вы этим ассоциированным значением не интересуетесь. Если необходимо, вы также можете добавить дополнительные ограничения с помощью предложения where.

Pattern matching while


Pattern matching можно также использовать с while циклом. В этом случае мы будем повторять тело цикла до тех пор, пока некоторое значение в предикате не будет соответствовать «образцу». Вот пример:



Заметим, что сложные предикаты, описанные в разделе «6. Сложные предикаты if» также поддерживаются циклом while, включая использование where.

Pattern для «развертывания» (unwrapping) многочисленных Optional


В Swift 1.2 у нас был прекрасный компактный синтаксис для «развертывания» множества Optionals в одном простом предложении if let:

var optional1: String?
var optional2: String?
  
if let optional1 = optional1, let optional2 = optional2 {
    print("Success")
} else {
    print("Failure")
}

Здорово!

Однако, вы все же встречаетесь с ситуацией, когда вам действительно нужно управлять различными комбинациями существующих / пропущенных Optional зачений. Одним из таких примеров является форма для заполнения полей username и password, причем пользователь не заполнил одно из них, и нажал кнопку «Submit«. В этом случае вам захочется показать специальную ошибку, чтобы уведомить пользователя, что конкретно пропущено. Для этого мы можем использовать в Swift 1.x pattern maкching!

var username: String?
var password: String?
  
switch (username, password) {
case let (.Some(username), .Some(password)):
    print("Success!")
case let (.Some(username), .None):
    print("Password is missing")
case let (.None, .Some(password)):
    print("Username is missing")
case (.None, .None):
    print("Both username and password are missing")
}

Это немного неуклюже, но мы пользовались этим с самого начала.

В Swift 2 синтаксис выглядит более понятным:



При первом взгляде смущает использование вопросительного знака ? для того, чтобы показать, что значение присутствует (особенно если это ассоциировать с идеей Optionals, когда значение может существовать, а может и не существовать), но нужно признать, что этот пример становится очень понятным в отличие от неуклюжего синтаксиса .Some(username).

Обработка ошибок


Чтобы понять новые возможности Swift, относящиеся к обработке ошибок, будет полезно вспомнить, что существует 3 способа, когда функция может заканчиваться аварийно (далее для краткости перейдем на жаргон и будем говорить «падать»):

  • многие функции могут «падать» по одной достаточно простой «врожденной» причине, например, когда вы пытаетесь преобразовать String в Int; такие случаи достаточно хорошо обрабатываются с помощью возвращения Optional значения;
  • на другом конце спектра находятся логические ошибки программирования, которые вызывают выход индекса массива за границы, непреемлемые условия и т.д., с ними очень тяжело иметь дело и мы не знаем, как можно ими управлять.
  • третий случай — это ошибки детализации, поправимые ошибки, например, такие, как не найден файл, или ошибка сети или пользователь уничтожил операцию (ситуационные ошибки).

Обработка ошибок третьего типа, связанных с ситуацией, — вот что пытается улучшить Swift 2.

Если мы рассмотрим типичную схему управления такими ошибками в Swift 1.х и Objective-C, то мы обнаруживаем схему, когда функция получает аргумент inout NSError? и возвращает Bool для представления успешного или ошибочного завершения операции:

//Локальная переменная error запоминает ошибку, если она возвращается
var error: NSError?
// success это Bool:
let success = someString.writeToURL(someURL,
                                    atomically: true,
                                    encoding: NSUTF8StringEncoding,
                                    error: &error)
if !success {
    // Выводится информация об ошибке error:
    println("Error writing to URL: \(error!)")
}

У этого подхода есть множество «темных» сторон, которые делают менее понятным, а что собственно метод делает, но более важно то, что требуется ручная реализация соглашений относительно того, что стоит за возвращаемым Bool. Если метод возвращает объект и получил ошибку, то он возвращает nil; если это булевское значение, то возвращается false и так далее. Вам нужно знать, с каким методом вы имеете дело, что проверять, является ли результат nil или false или что-то еще, когда метод содержит объект ошибки NSError?. Очень запутанный синтаксис. Все эти трудности связаны с тем, что Objective-C не мог возвращать множество значений из функции или метода и в случае, если нам нужно уведомить пользователя об ошибке, то предлагался такой укоренившийся способ ее обработки.

Swift 2 получил новое управление ошибками. Он использует синтаксис do-try-catch, который заменяет NSError. Давайте посмотрим как можно использовать этот новый синтаксис. Я буду рассматривать очень простой пример обработки таких ошибок, с которыми вполне справляются возвращаемые Optional значения, и для которых новый синтаксис вообщем-то не предназначен. Но простота этого примера позволит мне акцентировать ваше внимание именно на механизме «выбрасывания» и «ловле» ошибок, а не на сложности их семантического содержания. В конце я приведу реальный пример обработки данных, пришедших из сети.

Прежде чем ошибка может быть выброшена (throw) или поймана (catch), она должна быть определена. Вы можете определить ее в Swift 2 с помощью enum, который реализует новый протокол ErrorType:



Для того, чтобы функция



могла «выбрасывать» (throw) ошибку, нужно анонсировать в заголовке функции ключевое слово throws:



Теперь эта функция может выбрасывать ошибку, используя ключевое слово throw и ссылку на конкретный тип ошибки:


Если вы попытаетесь вызвать эту функцию, то компилятор выдаст ошибку: «Вызываемая функция выбрасывает ошибки, а обращение к ней не помечено ключевым словом try и нет обработки ошибок.»



Потому что функция объявила, что она способна выбрасывать ошибки, и вы должны «ловить» потенциальные ошибки. Давайте попробуем использовать ключевое слово try:



Этого оказалось недостаточно, компилятор сообщает нам, что требуется обработка ошибок, которая производится с помощью синтаксической конструкции do-try-catch:


Более того, в блоке do-try-catch у вас есть возможность «ловить» несколько ошибок:



Если смысловая часть ошибок вас не интересует, то вместо того, чтобы использовать конструкции do-try-catch, можно обращаться с интересующими нас значениями как с Optional:



aTry и aTrySuccess являются Optional, так что не забывайте их «разворачивать» перед использованием!

Иногда бывает метод, который может «падать» только в определенных обстоятельствах, и вы точно знаете, что он не «упадет» при вашем способе использования. Тогда вы можете использовать try!.

Если функция «выбрасывает» ошибку, то она возвращается незамедлительно. Но иногда нужно сделать некоторые действия, например, по освобождению ресурсов или закрытию файлов, прежде, чем функция вернется. В этой ситуации прекрасно работает уже знакомое нам ключевое слово defer. С ключевым словом defer можно определить блок кода, который всегда исполняется, если функция возвращается, и не имеет значения возвращается ли она нормально или из-за ошибок.

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



Перейдем к рассмотрению реального примера, представленного в статье Natasha Murashev. Swift 2.0: Let’s try?. Рассмотрим данные, которые приходят с некоторого API (после десериализации):



Эти данные нужно преобразовать в Модель для последующего использования в приложении:



Парсер TodoItemParser имеет дело со смешанными данными, пришедшими из некоторого API, преобразует их в понятную Модель для последующего безопасного использования в приложении и «бросает» ошибки, если их обнаружит:



Теперь выполним парсинг «хороших» данных в Модель с использованием конструкции do-try-catch



Выполним парсинг «плохих» данных.



Вместо использования конструкции do-try-catch, можно обращаться с интересующими нас значениями как с Optional с помощью оператора try?:



В первой части мы рассмотрели лишь часть новых возможностей Swift 2:

— фундаментальные конструкции языка, такие, как enum, scoping (область действия), синтаксис аргументов и т.д.
— сопоставление с образцом (pattern matching)
— управление ошибками (error handling)

Во второй части мы рассмотрим оставшиеся:

— проверка доступности (availability checking)
— расширения (extensions) протокола
— взаимодействие с Objective-C

Ссылки на используемые статьи:

New features in Swift 2
What I Like in Swift 2
A Beginner’s guide to Swift 2
Error Handling in Swift 2.0
Swift 2.0: Let’s try?
Video Tutorial: What’s New in Swift 2 Part 4: Pattern Matching
Throw What Don’t Throw
The Best of What’s New in Swift
Теги:
Хабы:
+24
Комментарии 36
Комментарии Комментарии 36

Публикации

Истории

Работа

Ближайшие события

Московский туристический хакатон
Дата 23 марта – 7 апреля
Место
Москва Онлайн
Геймтон «DatsEdenSpace» от DatsTeam
Дата 5 – 6 апреля
Время 17:00 – 20:00
Место
Онлайн
PG Bootcamp 2024
Дата 16 апреля
Время 09:30 – 21:00
Место
Минск Онлайн
EvaConf 2024
Дата 16 апреля
Время 11:00 – 16:00
Место
Москва Онлайн