Что нового в Swift 4.1?

Автор оригинала: Cosmin Pupăză
  • Перевод
Xcode 9.3 и Swift 4.1, наконец-то, уже не являются бета! Данный релиз содержит долгожданные улучшения стандартной библиотеки и самого языка. Если вы не следите за процессом Swift Evolution, то эта статья для Вас.

Swift 4.1

В этой статье вы узнаете о наиболее значительных изменениях, внесенных в Swift 4.1.

Эта статья требует наличия Xcode 9.3, поэтому убедитесь, что данная версия Xcode установлена.

Первые Шаги


Swift 4.1 совместим с исходным кодом Swift 4, поэтому новые функции не нарушают ваш код, если вы уже перенесли свой проект на Swift 4 с помощью Swift Migrator в Xcode.

В следующих разделах вы увидите связанные теги, такие как [SE-0001]. Это номера предложений Swift Evolution. Я добавил ссылку на каждое предложение, чтобы вы могли вникнуть в полную информацию о каждом конкретном изменении. Я рекомендую вам попробовать функции на практике используя Playground, что бы лучше понять, что именно поменяется в вашей работе.

Чтобы начать, запустите Xcode 9.3 и выберите File ▸ New ▸ Playground. Выберите iOS в качестве платформы и Blank в качестве шаблона. Назовите и сохраните его по своему усмотрению. Чтобы получить максимальную отдачу от этой статьи, попробуйте на практике каждую функцию в Playground.

Примечание: Если Вы пропустили что же было изменено в Swift 4 и собираетесь наверстать упущенное? Нет проблем! Ознакомьтесь с Swift 4 прочитав What’s New in Swift 4.

Улучшения языка


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

Условное соответствие


Условное соответствие допускает соответствие протокола для общих типов, где аргументы типа удовлетворяют определенные условия [SE-0143]. Это мощная функция, которая делает код более гибким. Вы сможете увидеть, как она работает на нескольких примерах.

Условное соответствие в стандартной библиотеке


В Swift 4 вы можете сравнивать массивы, словари и опционалы, если их элементы соответствуют протоколу Equatable. Это работало совершенно нормально для основных сценариев, таких как:

// Arrays of Int
let firstArray = [1, 2, 3]
let secondArray = [1, 2, 3]
let sameArray = firstArray == secondArray

// Dictionaries with Int values
let firstDictionary = ["Cosmin": 10, "George": 9]
let secondDictionary = ["Cosmin": 10, "George": 9]
let sameDictionary = firstDictionary == secondDictionary

// Comparing Int?
let firstOptional = firstDictionary["Cosmin"]
let secondOptional = secondDictionary["Cosmin"]
let sameOptional = firstOptional == secondOptional

Использование оператора == для проверки равенства в этих примерах было вполне справедливым, поскольку Int является Equatable в Swift 4. Однако сравнение наборов опционалов — обычная ситуация, с которой вы могли столкнуться в Swift 4, поскольку опционалы не соответствуют протоколу Equatable. Swift 4.1 исправляет эту проблему, используя условное соответствие, позволяя сравнивать дополнительные типы с теми типами, которые лежат в основе Equatable:

// Array of Int?
let firstArray = [1, nil, 2, nil, 3, nil]
let secondArray = [1, nil, 2, nil, 3, nil]
let sameArray = firstArray == secondArray

// Dictionary with Int? values
let firstDictionary = ["Cosmin": 10, "George": nil]
let secondDictionary = ["Cosmin": 10, "George": nil]
let sameDictionary = firstDictionary == secondDictionary

// Comparing Int?? (Optional of Optional)
let firstOptional = firstDictionary["Cosmin"]
let secondOptional = secondDictionary["Cosmin"]
let sameOptional = firstOptional == secondOptional

Int? является Equatable в Swift 4.1, поэтому оператор == работает для [Int?], [String: Int?] и Int??.

Аналогичная проблема была решена при сравнении массивов (например, [[Int]]). В Swift 4 вы можете сравнивать массивы множеств (например, [Set </Int/>]), так как множества соответствуют протоколу Equatable. Swift 4.1 решает это, потому как массивы (и словари), а также их базовые значения являются Equatable.

let firstArrayOfSets = [Set([1, 2, 3]), Set([1, 2, 3])]
let secondArrayOfSets = [Set([1, 2, 3]), Set([1, 2, 3])]

// Will work in Swift 4 and Swift 4.1
// since Set<Int> is Equatable
firstArrayOfSets == secondArrayOfSets

let firstArrayOfArrays = [[1, 2, 3], [3, 4, 5]]
let secondArrayOfArrays = [[1, 2, 3], [3, 4, 5]]

// Caused an error in Swift 4, but works in Swift 4.1
// since Arrays are Equatable in Swift 4.1
firstArrayOfArrays == secondArrayOfArrays

Как правило, Optional, Array и Dictionary в Swift 4.1 теперь соответствуют протоколам Equatable и Hashable, всякий раз, когда их базовые значения или элементы соответствуют этим протоколам.

Вот пример того, как условная соответствия работают в стандартной библиотеке. Затем вы реализуете это у себя в коде.

Условное соответствие на практике


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

// 1 
class LeadInstrument: Equatable {
  let brand: String
  
  init(brand: String) {
    self.brand = brand
  }
  
  func tune() -> String {
    return "Standard tuning."
  }
  
  static func ==(lhs: LeadInstrument, rhs: LeadInstrument) -> Bool {
    return lhs.brand == rhs.brand
  }
}

// 2
class Keyboard: LeadInstrument {
  override func tune() -> String {
    return "Keyboard standard tuning."
  }
}

// 3
class Guitar: LeadInstrument {
  override func tune() -> String {
    return "Guitar standard tuning."
  }
}

Вот, что этот код выполняет шаг за шагом:

  1. Класс LeadInstrument соответствует протоколу Equatable. Он имеет определенный brand и метод tune(), который вы будете использовать для настройки инструмента.
  2. Вы переопределяете метод tune() в классе Keyboard, чтобы вернуть стандартные настройки для объекта.
  3. Вы делаете то же самое для класса Guitar.

Затем объявите группу инструментов:

// 1  
class Band<LeadInstrument> {
  let name: String
  let lead: LeadInstrument
  
  init(name: String, lead: LeadInstrument) {
    self.name = name
    self.lead = lead
  }
}

// 2
extension Band: Equatable where LeadInstrument: Equatable {
  static func ==(lhs: Band<LeadInstrument>, rhs: Band<LeadInstrument>) -> Bool {
    return lhs.name == rhs.name && lhs.lead == rhs.lead
  }
}

Вот что вы делаете шаг за шагом:

  1. Вы создаете класс Band типа — LeadInstrument. Каждая группа имеет уникальное name(имя) и lead instrument(основной инструмент).
  2. Вы используете, where чтобы Band соответствовал протоколу Equatable, так же как и LeadInstrument выполняет определенные действия. Здесь и проявляется условное соответствие — вы можете присвоить соответствие протоколу Equatable для дженерика LeadInstruments.

Затем создайте свои любимые группы инструментов и сравните их:

// 1
let rolandKeyboard = Keyboard(brand: "Roland")
let rolandBand = Band(name: "Keys", lead: rolandKeyboard)
let yamahaKeyboard = Keyboard(brand: "Yamaha")
let yamahaBand = Band(name: "Keys", lead: yamahaKeyboard)
let sameBand = rolandBand == yamahaBand

// 2
let fenderGuitar = Guitar(brand: "Fender")
let fenderBand = Band(name: "Strings", lead: fenderGuitar)
let ibanezGuitar = Guitar(brand: "Ibanez")
let ibanezBand = Band(name: "Strings", lead: ibanezGuitar)
let sameBands = fenderBand == ibanezBand

В этом фрагменте кода вы создаете две Keyboards и Guitar вместе с их соответствующими Bands. Затем вы сравниваете Bands напрямую, благодаря условному соответствию, которое вы определили ранее.

Условное соответствие в разборе JSON


В Swift 4.1 массивы, словари, наборы и дополнительные модули соответствуют протоколу Codable, если их элементы также соответствуют этому протоколу. Добавьте следующий код в Playground:

struct Student: Codable, Hashable {
  let firstName: String
  let averageGrade: Int
}

let cosmin = Student(firstName: "Cosmin", averageGrade: 10)
let george = Student(firstName: "George", averageGrade: 9)
let encoder = JSONEncoder()

// Encode an Array of students
let students = [cosmin, george]
do {
  try encoder.encode(students)
} catch {
  print("Failed encoding students array: \(error)")
}

// Encode a Dictionary with student values
let studentsDictionary = ["Cosmin": cosmin, "George": george]
do {
  try encoder.encode(studentsDictionary)
} catch {
  print("Failed encoding students dictionary: \(error)")
}

// Encode a Set of students
let studentsSet: Set = [cosmin, george]
do {
  try encoder.encode(studentsSet)
} catch {
  print("Failed encoding students set: \(error)")
}

// Encode an Optional Student
let optionalStudent: Student? = cosmin
do {
  try encoder.encode(optionalStudent)
} catch {
  print("Failed encoding optional student: \(error)")
}

Вы используете этот код для encode [Student], [String: Student], Set</Student/> и Student?. Этот код хорошо работает в Swift 4.1, так как Student является Codable, что делает эти типы коллекций также соответствующими Codable.

Преобразование между CamelCase и Snake_Case при работе с JSON


Swift 4.1 позволяет преобразовывать свойства CamelCase в ключи snake_case во время парсинга JSON:

var jsonData = Data()
encoder.keyEncodingStrategy = .convertToSnakeCase
encoder.outputFormatting = .prettyPrinted

do {
  jsonData = try encoder.encode(students)
} catch {
  print(error)
}

if let jsonString = String(data: jsonData, encoding: .utf8) {
  print(jsonString)
}

При создании объекта encoder вы устанавливаете свойство keyEncodingStrategy равным .convertToSnakeCase. Посмотрев на консоль, вы должны увидеть:

[
  {
    "first_name" : "Cosmin",
    "average_grade" : 10
  },
  {
    "first_name" : "George",
    "average_grade" : 9
  }
]

Вы также можете конвертировать обратно из snake_case в CamelCase во время работы с JSON:

var studentsInfo: [Student] = []
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase

do {
  studentsInfo = try decoder.decode([Student].self, from: jsonData)
} catch {
  print(error)
}

for studentInfo in studentsInfo {
  print("\(studentInfo.firstName) \(studentInfo.averageGrade)")
} 

На этот раз для свойства keyDecodingStrategy вы присвоите значение .convertFromSnakeCase.

Соответствие и совместимость протоколов Equatable и Hashable


В Swift 4 требовалось писать шаблонный код, чтобы структуры соответствовали протоколам Equatable и Hashable:

struct Country: Hashable {
  let name: String
  let capital: String
  
  static func ==(lhs: Country, rhs: Country) -> Bool {
    return lhs.name == rhs.name && lhs.capital == rhs.capital
  }
  
  var hashValue: Int {
    return name.hashValue ^ capital.hashValue &* 16777619
  }
}

Используя этот код, вы реализуете == (lhs: rhs :) и hashValue для поддержки как Equatable, так и Hashable. Вы можете сравнивать объекты Country, добавлять их в Set’s и даже использовать их в качестве ключей для словаря:

let france = Country(name: "France", capital: "Paris")
let germany = Country(name: "Germany", capital: "Berlin")
let sameCountry = france == germany

let countries: Set = [france, germany]
let greetings = [france: "Bonjour", germany: "Guten Tag"]

Swift 4.1 добавляет реализации по умолчанию для структур соответствующих Equatable и Hashable, поскольку все ее свойства так же Equalable и Hashable [SE-0185].

Это очень упрощает ваш код, который можно просто переписать как:

struct Country: Hashable {
  let name: String
  let capital: String
}

Перечисления со связанными значениями также требуют дополнительный код для работы с Equatable и Hashable в Swift 4:

enum BlogPost: Hashable {
  case tutorial(String, String)
  case article(String, String)
  
  static func ==(lhs: BlogPost, rhs: BlogPost) -> Bool {
    switch (lhs, rhs) {
    case let (.tutorial(lhsTutorialTitle, lhsTutorialAuthor), .tutorial(rhsTutorialTitle, 
               rhsTutorialAuthor)):
      return lhsTutorialTitle == rhsTutorialTitle && lhsTutorialAuthor == rhsTutorialAuthor
    case let (.article(lhsArticleTitle, lhsArticleAuthor), .article(rhsArticleTitle, rhsArticleAuthor)):
      return lhsArticleTitle == rhsArticleTitle && lhsArticleAuthor == rhsArticleAuthor
    default:
      return false
    }
  }
  
  var hashValue: Int {
    switch self {
    case let .tutorial(tutorialTitle, tutorialAuthor):
      return tutorialTitle.hashValue ^ tutorialAuthor.hashValue &* 16777619
    case let .article(articleTitle, articleAuthor):
      return articleTitle.hashValue ^ articleAuthor.hashValue &* 16777619
    }
  }
}

Вы использовали кейсы перечисления для записи реализаций ==(lhs: rhs :) и hashValue. Это позволило вам сравнить сообщения в блогах и использовать их в наборах и словарях:

let swift3Article = BlogPost.article("What's New in Swift 3.1?", "Cosmin Pupăză")
let swift4Article = BlogPost.article("What's New in Swift 4.1?", "Cosmin Pupăză")
let sameArticle = swift3Article == swift4Article

let swiftArticlesSet: Set = [swift3Article, swift4Article]
let swiftArticlesDictionary = [swift3Article: "Swift 3.1 article", swift4Article: "Swift 4.1 article"]

В отличии от Hashable, размер этого кода является значительно меньшим в Swift 4.1, благодаря реализациям Equatable и Hashable:

enum BlogPost: Hashable {
  case tutorial(String, String)
  case article(String, String)
}

Вы просто спасли себя от работы с 20 строками кода шаблона!

image

Hashable Index Types


Key paths могли использовать индексы, если тип параметра индекса был Hashable в Swift 4. Это позволило им работать с массивами double; например:

let swiftVersions = [3, 3.1, 4, 4.1]
let path = \[Double].[swiftVersions.count - 1]
let latestVersion = swiftVersions[keyPath: path]

Вы используете keyPath для получения текущего номера версии Swift из swiftVersions.

Swift 4.1 добавляет соответствие Hashable ко всем типам нижних индексов в стандартной библиотеке [SE-0188]:

let me = "Cosmin"
let newPath = \String.[me.startIndex]
let myInitial = me[keyPath: newPath]

По индекс возвращается первая букву строки. Он работает, поскольку типы индекса String являются Hashable в Swift 4.1.

Рекурсивные ограничения на связанных типах в протоколах


Swift 4 не поддерживал определение рекурсивных ограничений на связанных типах в протоколах:

protocol Phone {
  associatedtype Version
  associatedtype SmartPhone
}

class IPhone: Phone {
  typealias Version = String
  typealias SmartPhone = IPhone
}

В этом примере вы определили тип связанный с SmartPhone, но, возможно, оказалось бы полезным ограничить его до Phone, поскольку все смартфоны являются телефонами. Теперь это возможно в Swift 4.1 [SE-0157]:

protocol Phone {
  associatedtype Version
  associatedtype SmartPhone: Phone where SmartPhone.Version == Version, SmartPhone.SmartPhone == SmartPhone
}

Вы используете, where чтобы ограничить как Version, так и SmartPhone, чтобы они были такими же, как у телефона.

Слабые и не занятые ссылки в протоколах


Swift 4 поддерживает weak и не unowned свойства протокола:

class Key {}
class Pitch {}

protocol Tune {
  unowned var key: Key { get set }
  weak var pitch: Pitch? { get set }
}

class Instrument: Tune {
  var key: Key
  var pitch: Pitch?
  
  init(key: Key, pitch: Pitch?) {
    self.key = key
    self.pitch = pitch
  }
}

Вы настроили инструмент в определенном key и pitch. Шаг, возможно, был нулевым, так что вы будете моделировать его, как weak в протоколе Tune.

Но как weak, так и unowned практически бессмысленны, если они определены в самом протоколе, поэтому Swift 4.1 удаляет их, и вы получите предупреждение, используя эти ключевые слова в протоколе [SE-0186]:

protocol Tune {
  var key: Key { get set }
  var pitch: Pitch? { get set }
}

Index Distances in Collections


Swift 4 использовал IndexDistance для объявления количества элементов в коллекции:

func typeOfCollection<C: Collection>(_ collection: C) -> (String, C.IndexDistance) {
  let collectionType: String
  
  switch collection.count {
  case 0...100:
    collectionType = "small"
  case 101...1000:
    collectionType = "medium"
  case 1001...:
    collectionType = "big"
  default:
    collectionType = "unknown"
  }
  
  return (collectionType, collection.count)
}

Метод typeOfCollection(_ :) возвращает кортеж, который содержит тип и количество коллекции. Вы можете использовать его для любых коллекций, таких как массивы, словари или наборы; например:

typeOfCollection(1...800) // ("medium", 800)
typeOfCollection(greetings) // ("small", 2)

Вы можете улучшить возвращаемый тип функции путем ограничения IndexDistance на Int с предложением where:

func typeOfCollection<C: Collection>(_ collection: C) -> (String, Int) where C.IndexDistance == Int {
  // тот же код, что и в приведенном выше примере
}

Swift 4.1 заменяет IndexDistance на Int в стандартной библиотеке, поэтому в этом случае вам не нужно предложение where [SE-0191]:

func typeOfCollection<C: Collection>(_ collection: C) -> (String, Int) {
  // тот же код, что и в приведенном выше примере
}

Инициализаторы структуры в модулях


Добавление свойств к структурам public может привести к исходным изменениям в Swift 4. В этой статьи убедитесь, что Project Navigator виден в Xcode, перейдя в меню View\Navigators\Show Project Navigator. Затем щелкните правой кнопкой мыши «Sources» и выберите «New File» в меню. Переименуйте файл DiceKit.swift. Замените его содержимое следующим блоком кода:

public struct Dice {
  public let firstDie: Int
  public let secondDie: Int

  public init(_ value: Int) {
    let finalValue: Int

    switch value {
    case ..<1:
      finalValue = 1
    case 6...:
      finalValue = 6
    default:
      finalValue = value
    }

    firstDie = finalValue
    secondDie = 7 - finalValue
  }
}

Инициализатор структуры гарантирует, что обе игральные кости имеют действительные значения между 1 и 6. Вернитесь в Playground и добавьте в конце этот код:

// 1
let dice = Dice(0)
dice.firstDie
dice.secondDie

// 2
extension Dice {
  init(_ firstValue: Int, _ secondValue: Int) {
    firstDie = firstValue
    secondDie = secondValue
  }
}

// 3
let newDice = Dice(0, 7)
newDice.firstDie
newDice.secondDie

Вот что вы сделали с этим кодом:

  1. Вы создали валидную пару игральных кубиков.
  2. Вы добавили Dice посредством другого инициализатора, который имеет прямой доступ к его свойствам .
  3. Вы определили недействительную пару игральных кубиков с новым инициализатором структуры.

В Swift 4.1 перекрестные целевые инициализаторы (cross-target initializers) должны вызывать значение по умолчанию. Измените расширение Dice на:

extension Dice {
  init(_ firstValue: Int, _ secondValue: Int) {
    self.init(abs(firstValue - secondValue))
  }
}

Это изменение приводит к тому, что структуры ведут себя как классы: инициализаторы кросс-модулей должны быть инициализаторами удобства в Swift 4.1 [SE-0189].

image

В Swift 4.1 вы больше не сможете обманывать игру в кости!

Настройки платформы и обновления конфигурации


Swift 4.1 добавляет некоторые необходимые функции платформы и сборки для тестирования кода:

Сборка импортов/Build Imports


В Swift 4 вы тестировали модуль если он доступен на определенной платформе, определяя саму операционную систему: например:

#if os(iOS) || os(tvOS)
  import UIKit
  print("UIKit is available on this platform.")
#else
  print("UIKit is not available on this platform.")
#endif

UIKit доступен на iOS и tvOS, поэтому вы импортировали его, если тест был пройден успешно. Swift 4.1 упрощает этот процесс, позволяя вам проверить сам модуль:

#if canImport(UIKit)
print("UIKit is available if this is printed!")
#endif

В Swift 4.1 вы используете #if canImport (UIKit), чтобы подтвердить, что определенная структура доступна для импорта [SE-0075].

Target Environments


При написании кода в Swift 4 самым известным способом проверки выполнения кода на симуляторе или физическом устройстве, была проверка архитектуры и операционной системы:

#if (arch(i386) || arch(x86_64)) && (os(iOS) || os(tvOS) || os(watchOS))
  print("Testing in the simulator.")
#else
  print("Testing on the device.")
#endif

Была ли архитектура вашего процессора построена на базе Intel, а ваша операционная система — iOS, tvOS или watchOS, вы тестировали в симуляторе. В ином случае вы тестировали устройство.
Этот тест был очень громоздким, и он также в полной мере не описывал тип ошибок. Swift 4.1 делает этот тест более простым; просто используйте targetEnvironment (simulator) [ SE-0190 ] следующим образом:

#if targetEnvironment(simulator)
  print("Testing in the simulator.")
#endif

Miscellaneous Bits and Pieces


В Swift 4.1 есть еще несколько обновлений, которые стоит знать:

Compacting Sequences/Последовательности сжатия


В Swift 4 было довольно распространено использование flatMap(_ :) для фильтрации значений nil из последовательности:

let pets = ["Sclip", nil, "Nori", nil]
let petNames = pets.flatMap { $0 } // ["Sclip", "Nori"]

К сожалению, flatMap(_ :) был перегружен различными способами и, в этом конкретном сценарии, присвоение flatMap(_ :) не очень описывало предпринятые действия.

По этим причинам Swift 4.1 представляет переименование flatMap(_ :) в compactMap(_ :), чтобы сделать его смысл более понятным и уникальным [SE-0187]:

let petNames = pets.compactMap {$ 0}

Unsafe Pointers/Небезопасные указатели


Swift 4 использовал временные небезопасные изменяемые указатели для создания и изменения небезопасных изменяемых указателей буфера:

let buffer = UnsafeMutableBufferPointer<Int>(start: UnsafeMutablePointer<Int>.allocate(capacity: 10), 
                                             count: 10)
let mutableBuffer = UnsafeMutableBufferPointer(start: UnsafeMutablePointer(mutating: buffer.baseAddress), 
                                               count: buffer.count)

Swift 4.1 позволяет работать с небезопасными изменяемыми указателями буфера напрямую, используя тот же подход, что и с небезопасными изменяемыми указателями [SE-0184]:

Новые возможности Playground


Swift 4 позволил вам настроить описания типов в Playground Xcode:

class Tutorial {}
extension Tutorial: CustomPlaygroundQuickLookable {
  var customPlaygroundQuickLook: PlaygroundQuickLook {
    return .text("raywenderlich.com tutorial")
  }
}
let tutorial = Tutorial()

Вы реализовали CustomPlaygroundQuickLookable для Tutorial и возвращения краткого описания. Тип описания в customPlaygroundQuickLook был ограничен случаями PlaygroundQuickLook. такого каламбура в Swift 4.1 уже нету:

extension Tutorial: CustomPlaygroundDisplayConvertible {
  var playgroundDescription: Any {
    return "raywenderlich.com tutorial"
  }
}

На этот раз вы реализуете CustomPlaygroundDisplayConvertible. Тип описания — Any, поэтому вы можете вернуть все, что угодно из playgroundDescription. Это упрощает ваш код и делает его более гибким [SE-0198].

Что далее?


Swift 4.1 улучшает некоторые функции Swift 4 в подготовке к более серьезным изменениям, которые появятся в Swift 5 в этом году. К ним относятся стабильность ABI, улучшенные дженерики и строки, новые модели владения памятью и параллелизма и многое другое.

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

Если вам интересно, какие изменения будут в Swift 5, мы также рекомендуем вам ознакомиться с предложениями Swift Evolution, где вы можете увидеть новые функции, изменения и дополнения.
AdBlock похитил этот баннер, но баннеры не зубы — отрастут

Подробнее
Реклама

Комментарии 3

    0
    Спасибо за перевод.
    Когда они там уже перестанут выкатывать каждый год новую версию, и можно будет хоть немного расслабиться? Неужели обязательно так часто выкатывать обновления на уровне языка? Не к вам вопрос, конечно. Просто мысли вслух.
      0
      Это чтоб ты не расслаблялся и держался в тонусе. Все правильно они делают
        0
        Язык просто еще очень молодой. Потому это еще терпеть года 3 так точно. Потом все стабилизируется и будет счастье нам всем.

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

      Самое читаемое