Scala для С# разработчиков

C#, Scala,

Всем привет.
Я думаю все замечали за собой, что когда долго пишешь код на одном языке то при переключении на другой обязательно пытаешься их сравнивать. Я решил описать эти попытки сравнения, вдруг комуто поможет, что не привычно и непонятно при переходе с C# на Scala, синтаксис, сравним типы, нюансы.

Scala является полноценным функциональным языком, рассматрим ее объектно-ориентированную сторону т.к. это ближе к C#.

За ранее предупреждаю я ни коем образом не хочу агитировать за тот или другой язык, я просто постараюсь сравнить глазами разработчика на С#, мир не должен сойтись клином на одном языке, т.к. опыт работы с другими часто помогает писать лучше на своем основном.

Первые "принципы" Scala которые надо освоить, начав работать с данным языком.

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

Не должно быть null обьектов
Scala имеет средства борьбы с этим злом :)

Нет операторов таких как: return, break, continue, swith(){ case }
Здесь конечно могут возникнуть некоторые затруднения, привычка от которой надо избавляться на время работы со Scala, стараться проектировать методы так чтобы нам не нужны были выходы из него во множестве мест. Как плюс можно отметить, что данных подход в последствии даст более ясный и чистый код "хотя чисто мое субьективное мнение :)". P.S. return есть в Scala но пользоваться им не рекомендуют.

Нет static т.е. нет статик классов или полей или свойств вообще нет статики.
Довольно часто такие обьекты приносят больше вреда чем пользы. В Scala пошли другим путем, статиков нет но есть специальный обьект который является реализацией паттерна Signelton.

if-else и for не statements а expressions.
Это просто обалденная вещь :) Для if-else это означает, что он своего рода функция которая принимает предикат if(predicate) похожая ситуация и for. Можно написать вот так!
val x = if (a > b) a else b

По умолчанию в Scala все классы public и все методы vitrual
Обьяснять по этому пункту я думаю не нужно, решайте сами удобно Вам это или нет, но помнить об этом нужно.

Сравнение Scala - C#.

class
Как в C# мы не можем наследоваться от множества классов, мы должны создать обьект с помощью new, и можно создавать abstract class.

trait (признак, примесь)
Вот тут возникают некоторые сложности при сравнении с C#.
  • Во первых, можно частично сравнить с C# interface:
    • Scala класс может наследоваться от одного класса и множества trait это называется mixing
    • Можно описывать абстрактные методы/свойства для последующей реализации в наследумых классах
    • Нельзя создать конструктор
  • Во вторых, можно сравнить с С# abstract class:
    • Можно создавать как абстрактные так реализованные методы/свойства
    • Нельзя создавать обьект через new
    • Может наследоваться от других классов

object
Это механизм борьбы со static. Можно думать об object как о скрытом от нас реализации паттерна Signelton. Я не буду вдаваться в подробности данного паттерна, но все его свойства присутствуют при создании object и не надо делать ни каких лишних тело движений.

Еще некоторые свойства которые надо знать.

  • Можно наследоваться от класса и множества trait
  • Нельзя наследоваться от другого object

case class
Рассмотрим его как обычный класс со следующими плюшками :)
  • можно создавать обьект класса без оператора new
  • можно использовать case class для match
  • хорошей практикой использования case class является инициализация его через конструктор с параметрами т.к. он автоматически имплементирует методы toString, equals, hashCode на основе аргументов конструктора. Также подразумевается использвать его как обьект хранения неизменяемых данных.

Подытожим этот абзац еще некоторой интересной информацией, Scala не имеет partial class, и в этом нет особой нужды т.к. в trait есть возможность реализовать функционал, прибавляем к этому наследование от множества trait и можно просто спроектировать разделение функционала с помощью наследования. Возникает ли проблема ромба при множественного наследования, в Scala это как раз и решается с помощью trait правильней сказать они были задуманы для решения проблем множественного наследования. trait занимает промежуточное место между интерфейсом и классом и он подмешиваеться в класс (как это происходит мы увидим ниже). Важным моментов для trait является порядок наследования, подробней об этом можно почитать если искать по фразе Scala''s Stackable Trait Pattern. Коротко для тех кому лень, если класс наследуется от множества в котором классы или реализации какимто образом повторяются то последние подмешанные trait в списке наследования перекроют предыдущие т.е. (Trait1, Trait2, Trait3, Trait2) -> (Trait1, Trait3, Trait2).

Таблица соответствий типов Scala - C#.

С#ScalaScala Описание
byteByte8 bit -128 to 127
shortShort16 bit -32768 to 32767
intInt32 bit -2147483648 to 2147483647
longLong64 bit -9223372036854775808 to 9223372036854775807
floatFloat32 bit
doubleDouble64 bit
charChar16 bit
stringString
boolBooleantrue/false
с этим типом сложнее, можно ассоциировать его с void но у Unitимеет больше возможностей Unit означает что функция возвращает пустое значение (но все таки значение).
nullNullnull это trait т.е. это ссылочный тип и им можно обнулить любой ссылочный обьект
Nothing это тоже trait, он более обобщенный чем Null, обычно такое сильно обобщенное "ничто" используется для методов выбрасывающих исключение
def error(message: String): Nothing = throw new RuntimeException(message)
ну собственно по этой причине я употребил фразу "все таки значение" при описании Unit. Nothing не имеет значения, и обычно сигнализирует, что что то пошло не так.
Nilэто обьект обозначающий пустую коллекцию
AnyValсупер тип для значимых типов
AnyRefсупер тип для ссылочных типов

Добавлю к ней цепочку конвертации AnyVal типов, это бывает полезно знать :)

Byte -> Double -> Short -> Int -> Long -> Float -> Double
Char -> Int

Практика

Чтобы быстрее понять синтаксис расмотрим код Scala и C#.

C#

public class User {
    public string Name { get; set; }
    public int Age { get; set; }
    public string Phone { get; set; }
    
    public User(string name, int age, string phone = "") {
        Phone = phone;
        Age = age;
        Name = name;
    }
}
var user =  new User("Name",18);
var name = user.Name;

Scala

class User(var name:String,var age:Int,var phone:String = "")

val user = new User("Name",18) val name = user.Name /* or => val name = user Name */

Из данного примера видим как "магическим" способом 11 строк кода превратились в одну, и я предлагаю разобрать этот пример по частям и немного углубиться в понимании процесса.

Создание конструктора
Нам не надо создавать отдельный блок для конструктора, конструктор прописывается в () рядом и именем класса
Описание переменных
Нет нужды описывать переменные внутри класса т.к. Scala создает публичные поля по переменным проинициализированым в конструкторе, соответственно отпадает необходимость в установке значений с констукторе.
Getter & Setter
Как Вы могли заменить я создал поля использовав get; set; в Scala это заменяется оператором var
Значение по умолчанию
С этим я думаю должно быть понятно т.к. синтаксис похож на C#
Также нужно всегда указывать тип явно.
Readonly поля
Readonly поля определяются оператором val
; в конце строки
В Scala нет необходимости ставить точку с запятой в конце выражения т.к. компилятор сам опеределяет конец выражения. Ни кто конечно вам не запрещает их ствать, но они нужны только в ситуации когда необходимо написать несколько выражений в одну строчку.
            val expr1 = 1 + 2; val expr2 = expr2 + 5
        
Оператор доступа к членам
Мы можем использовать как . так и пробел, выбирайте сами что Вам ближе по душе. Считается, что нажать пробел "быстрее/проще" и можно сократить время написание кода, но не факт :) плюс читабельность может пострадать.

Рассмотрим еще примеры инициализации.

val name : String = "Name"
//в большинстве случаев нам не нужно писать тип явно, Scala может это сделать за нас
val name = "Name" //=> name : String

Инициализация полей с помощью val или var как вы заметили не отличается.

Методы в Scala инициализируются с помощью оператора def т.е definition

def init() : Unit = { /* code */ }
// как и в примере с переменными возвращаемый тип Unit можно упустить 
def init() = { /* code */ }
В примере выше метод init не принимает параметры по этому скобки можно тоже упустить, и знак равно т.к. метод ни ничего не возвращает.
def init { /* code */ }

От определения метода (со скобками или без) зависит то так Вы его и будете вызывать.

И еще пару примеров определения методов для закрепления. Я немного забегу вперед потому, что я не смогу закончить рассказ про методы, если не расскажу как определять возвращаемый тип без оператора return, но в Scala все просто, тип возращаемого значения определятся автоматически из контекста. Простой пример, это когда код метода заканчивается определением которое возвращает какое либо значение, то Scala определит, что данное значение и будет возвращено методом.

//если метод возвращает значение, то оператор равно упускать нельзя, это будет ошибкой
//явно указываем возвращаемый тим
def addition(a : Int, b : Int ) : Int = { a + b }
// в данном примере тип определяется автоматически и будет тоже Int
def addition(a : Int, b : Int ) = { a + b }
//если в фигурных скобках простое выражение можно сами скобки упустить
def addition(a : Int, b : Int ) = a + b
Надо также помнить, что вызов функций и переменных может быть одинаковым, Вы должны сами за этим следить если это необходимо.
class UserVal {
    val name = "Name"
}
class UserDef {
    def name = "Name"
}
    
val n1 = new UserVal().name
val n2 = new UserDef().name
Давайте посмотрим примеры создания trait, object, case class и наследование, хотя синтаксис инициализации у них одинаковый, но наглядный пример никогда не помешает.
trait Tr { }
object Obj { }
case class Cs(val someVal : Int = 0) 

В C# мы добавляем наследование с помощью оператора : и далее через , перечисляем базовые class/interface. В Scala похожая ситуация только операторы отличаются, чтобы добавить базовый class/trait нужно использовать оператор extends и имя базового обьекта, а все последующие перечисляем через оператор with. Действуют теже правила, что и для C#, первым базовым обьектом должен идти класс потом интерфейсы, в Scala так же, сначала класс потом trait.

class Base { }

trait Tr extends Base { } trait Tr2 { } case class Cs(val someVal : Int = 0) extends Base with Tr2

Заключение

Вот собственно и всё что я хотел Вам рассказать в первой статье. Пишите что Вам понравилось или не понравилось в комментариях. Eсли Вы нашли ошибки в статье напишите об этом пожалуйста чтобы я мог исправить и не вводить ни кого в заблуждение.

Всем хорошего дня, спасибо.