Осваиваем новые языки программирования, опираясь на уже изученные

https://blog.usejournal.com/learning-new-programming-languages-by-building-on-existing-foundations-711c39587492
  • Перевод
Здравствуйте, коллеги.



Снимок Дженни Марвин с сайта Unsplash

Сегодня мы подготовили для вас перевод статьи о принципиальном сходстве многих языков программирования на примере с Ruby и C#. Надеемся, что идеи уважаемого Северина Переса помогут многим из вас поскорее приступить к изучению нового языка программирования, и дело пойдет с толком и удовольствием.

Чего не отнять у программиста – он никогда не прекращает учиться. У вас может быть любимый язык, или фреймворк, или библиотека, но можно не сомневаться, что исключительно ими вы обойтись не сможете. Вам может нравиться JavaScript, но на проекте, где вы сейчас работаете, может потребоваться Python. Возможно, вы искусны в Perl, но база кода в вашей компании может быть написана на C++. Для разработчика-новобранца идея изучить новый язык может показаться устрашающей, особенно если надвигаются дедлайны. Это плохая новость. Однако, есть и хорошая: изучить новый язык обычно не так сложно. Если взять за основу уже имеющиеся ментальные модели, то вы убедитесь, что изучение нового языка – это, в основном, расширение уже имеющихся знаний, а не работа с нуля.

Что у них похожего


Большинство языков программирования в основе своей базируются на одной и той же совокупности ключевых принципов. Реализация отличается, но едва ли найдутся два столь несхожих языка, что между ними не удастся провести параллели. Чтобы выучить и понять новый язык, самое важное – выявить, чем он похож на уже известные вам, а затем добрать новые знания, расширив представления о нем, когда/если это необходимо. Рассмотрим, например, типы данных и переменные. В каждом языке существует способ определять и хранить данные – единообразно во всей программе. Поэтому при изучении нового языка вам в первую очередь понадобится понять, как здесь определяются и используются переменные. Возьмем в качестве примера два разных языка: интерпретируемый Ruby с динамической типизацией и компилируемый C# со статической типизацией.

my_int = 8
my_decimal = 8.5
my_string = "electron"

puts "My int is: #{my_int}"
puts "My float is: #{my_decimal}"
puts "My string is: #{my_string}"

Аналогичный пример:

using System;

public class Program
{
    public static void Main()
    {
        int myInt = 8;
        double myDecimal = 8.5;
        string myString = "electron";

        Console.WriteLine("My int is: {0}", myInt);
        Console.WriteLine("My float is: {0}", myDecimal);
        Console.WriteLine("My string is: {0}", myString);
    }
}

Допустим, вы опытный Ruby-разработчик и хотите изучить C#. Ниже приведены фрагменты кода, в одном из которых вы без труда узнаете Ruby. Там всего лишь потребуется определить несколько переменных и вывести их в консоль. Теперь обратите внимание на второй фрагмент. Узнаете что-нибудь? Синтаксис отличается, но не возникает сомнений, что второй код работает примерно как и первый. Несколько раз встречается оператор =, который наверняка бросается в глаза как символ операций присваивания. Затем вызывается некий Console.WriteLine(), подразумевающий, что значения будут выводиться в консоль. Также здесь есть несколько строк, в которых, по-видимому, используется интерполяция для составления сообщений. Концептуально здесь нет ничего особо удивительного – присваивание, интерполяция, вывод в консоль, все эти операции уже известны вам по работе с Ruby.

Возможно, второй фрагмент кода вы поймете и без всяких знаний C#, но здесь определенно есть простор для расширения вашей ментальной модели. Например, почему все обертывается в метод Main()? Что это за ключевые слова int, double и string? С этого и начинается обучение. Вы уже в общих чертах понимаете, что здесь происходит (присваивание, интерполяция, вывод), теперь время переходить к деталям:

  • Main(): Немного вникнув в ситуацию, выясняем, что метод Main() – это входная точка, с которой начинается выполнение программы. Теперь мы знаем, что во всех программах на C# нам понадобится метод Main().
  • Переменные: в первой части нашего фрагмента на C# определенно происходит некое присваивание. Учитывая номенклатуру, вы, вероятно, догадываетесь, что ключевое слово int означает целочисленную переменную, double – число двойной точности с плавающей точкой, а string — строковую переменную. Практически сразу вы догадаетесь, что в C#, в отличие от Ruby, требуется статическая типизация переменных, поскольку переменные для разных типов данных объявляются по-разному. Почитав документацию, поймете, насколько по-разному.
  • Console.WriteLine(): Наконец, запустив программу, вы увидите, что Console.WriteLine() выводит значения в консоль. Из Ruby вам известно, что puts – это метод глобального объекта $stdout, а, сверившись с документацией по Console.WriteLine(), вы узнаете, что Console – это класс из пространства имен System, а WriteLine() – метод, определяемый в данном классе. Это не только очень напоминает puts, но и подсказывает, что C#, как и Ruby – это объектно-ориентированный язык. Вот вам и еще одна ментальная модель, которая поможет проследить новые параллели.

Вышеприведенный пример очень прост, но даже из него можно сделать ряд важных выводов. Вы уже узнали, что в программе на C# требуется четко определенная точка входа, что этот язык статически типизирован (в отличие от Ruby) и объектно-ориентирован (как и Ruby). Вы это выяснили, поскольку уже представляете, что такое переменные и методы, а далее расширили эти ментальные модели, обогатив их феноменом типизации.

Поиск отличий


Начиная примериваться, как читать и писать код на новом языке, первым делом нужно выяснить, какие вещи уже известны и могут послужить вам базисом для обучения. Далее переходите к отличиям. Вернемся к нашему переходу с Ruby на C# и рассмотрим кое-что посложнее.

particles = ["electron", "proton", "neturon"]

particles.push("muon")
particles.push("photon")

particles.each do |particle|
  puts particle
end

В этом фрагменте на Ruby определим массив под названием particles, в котором будет несколько строк, а затем воспользуемся Array#push, чтобы добавить в него еще несколько строк, и Array#each для перебора массива и вывода каждой отдельной строки в консоль. Но как же сделать то же самое на C#? Немного погуглив, узнаем, что в C# есть типизированные массивы (типизация уже не должна вас удивлять, учитывая, что вы выучили ранее), а также есть метод SetValue, который слегка напоминает push, но принимает в качестве параметров значение и позицию в индексе. В таком случае, первая попытка переписать на C# код Ruby может получиться такой:

using System;
using System.Collections.Generic;

public class Program
{
    public static void Main()
    {
        string[] particles = new string[] { "electron", "proton", "neturon" };
    
        particles.SetValue("muon", 3);
            // Исключение времени выполнения (строка 11): индекс выходит за пределы массива
        particles.SetValue("photon", 4);
		
        foreach (string particle in particles)
        {
            Console.WriteLine(particle);
        }
    }
}

К сожалению, этот код даст исключение времени выполнения Run-time exception, когда вы попытаетесь воспользоваться SetValue, чтобы добавить новое значение в массив. Снова смотрим документацию и выясняем, что массивы в C# не динамические, и должны инициализироваться либо сразу со всеми значениями, либо с указанием длины. Вновь попытавшись воспроизвести код Ruby, учтем это и получим следующий вариант:

using System;
using System.Collections.Generic;

public class Program
{
    public static void Main()
    {
        string[] particles = new string[] { "electron", "proton", "neturon", null, null };
        
        particles.SetValue("muon", 3);
        particles.SetValue("photon", 4);
		
        foreach (string particle in particles)
        {
            Console.WriteLine(particle);
        }
    }
}

Этот фрагмент действительно воспроизводит весь функционал исходного кода Ruby, но с большой натяжкой: просто он выводит в консоль те же самые значения. Если внимательнее рассмотреть оба фрагмента, то быстро обнаруживается проблема: во фрагменте на C# в массиве particles может быть не более 5 значений, тогда как во фрагменте на Ruby их допускается сколько угодно. Тогда становится ясно, что массивы в Ruby и C# принципиально отличаются: у первого динамический размер, а у второго нет. Чтобы как следует воспроизвести на C# функционал фрагмента на Ruby, нужен скорее такой код:

using System;
using System.Collections.Generic;

public class Program
{
    public static void Main()
    {
        List<String> particles = new List<String>();
        particles.Add("electron");
        particles.Add("proton");
        particles.Add("neutron");
        particles.Add("muon");
        particles.Add("photon");
		
        foreach (string particle in particles)
        {
            Console.WriteLine(particle);
        }
    }
}

Здесь используется структура данных List, позволяющая динамически собирать значения. В таком случае мы фактически воспроизводим на C# оригинальный код Ruby, но, что еще важнее, здесь мы можем оценить ключевое отличие между двумя языками. Хотя, в обоих языках используется термин «массив» и может показаться, что эти массивы – одно и то же, на практике они весьма отличаются. Вот вам и еще один момент, помогающий расширить ментальную модель, полнее представлять, что такое «массив», и как он устроен. В C# массив как структура данных может подойти или не подойти в ситуациях, где в Ruby вы прибегли бы именно к массивам; речь о ситуациях, где критично динамическое изменение размеров массива. Теперь вам придется заботиться об этом заранее и соответствующим образом продумывать ваш код.

Возвращаясь к ключевым принципам


Очень удобно приступать к изучению новых языков, исследуя их сходства и отличия по сравнению с уже известными языками; однако, в некоторых случаях целесообразнее начинать с универсальных принципов. Выше мы логически заключили, что C# — объектно-ориентированный язык, когда поработали со встроенным классом и одним из его методов, System.Console.WriteLine(), с помощью которого выполнили действие. Логично предположить, что в C#, как и в других объектно-ориентированных языках, предусмотрен механизм определения класса и инстанцирования объектов из него. Это базовый принцип объектно-ориентированного программирования, поэтому можно практически не сомневаться в верности нашего предположения. Для начала рассмотрим, как эта операция могла бы выглядеть в известном нам языке Ruby.

class Element
  attr_accessor :name, :symbol, :number
  
  def initialize(name, symbol, number)
    self.name = name
    self.symbol = symbol
    self.number = number
  end
  
  def describe
    puts "#{self.name} (#{self.symbol}) has atomic number #{self.number}."
  end
end

hydrogen = Element.new("Hydrogen", "H", 1)
hydrogen.describe

Здесь у нас простой класс Element, в котором есть метод конструктора, чтобы принимать значения и присваивать их инстанцированным объектам, набор методов доступа для установки и получения значений, а также метод экземпляра для вывода этих значений. В данном случае ключевыми концепциями являются идея класса, идея метода конструктора, идея геттеров/сеттеров и идея метода экземпляра. Возвращаясь к нашим представлениям о том, что можно делать в объектно-ориентированных языках, рассмотрим, как сделать то же самое на C#.

using System;

public class Program
{
    public static void Main()
    {
        Element hydrogen = new Element("Hydrogen", "H", 1);
        hydrogen.Describe();
    }
	
    public class Element
    {
        public string Name { get; set; }
        public string Symbol { get; set; }
        public int Number { get; set; }

        public Element(string name, string symbol, int number)
        {
            this.Name = name;
            this.Symbol = symbol;
            this.Number = number;
        }

        public void Describe()
        {
            Console.WriteLine
            (
                "{0} ({1}) has atomic number {2}.",
                this.Name, this.Symbol, this.Number
            );
        }
    }
}

Изучив этот фрагмент на C#, видим, что, на самом деле, он не так уж и отличается от варианта на Ruby. Определяем класс, при помощи конструктора указываем, как класс будет инстанцировать объекты, определяем геттеры/сеттеры и определяем метод экземпляра, который будем вызывать в создаваемых объектах. Естественно, два фрагмента на вид довольно отличаются, но не самым неожиданным образом. В версии на C# мы ссылаемся при помощи this на инстанцируемый объект, тогда как в Ruby для этого используется self. Версия на C# типизирована и на уровне методов, и на уровне параметров, а в Ruby – нет. Однако, на уровне ключевых принципов оба фрагмента практически идентичны.

Развивая эту тему, можно рассмотреть идею наследования. Известно, что наследование и образование подклассов – ключевые моменты объектно-ориентированного программирования, поэтому не составляет труда понять, что в C# это делается с тем же успехом, что и в Ruby.

class Element
  attr_accessor :name, :symbol, :number
  
  def initialize(name, symbol, number)
    self.name = name
    self.symbol = symbol
    self.number = number
  end
  
  def describe
    puts "#{self.name} (#{self.symbol}) has atomic number #{self.number}."
  end
end

class NobleGas < Element
  attr_accessor :category, :type, :reactivity
  
  def initialize(name, symbol, number)
    super(name, symbol, number)
    
    self.category = "gas"
    self.type = "noble gas"
    self.reactivity = "low"
  end
  
  def describe
    puts "#{self.name} (#{self.symbol}; #{self.number}) is a #{self.category} " +
         "of type #{self.type}. It has #{self.reactivity} reactivity."
  end
end

argon = NobleGas.new("Argon", "Ar", 18)
argon.describe

В версии на Ruby определяем подкласс NobleGas, наследующий от нашего класса Element; в его конструкторе используется ключевое слово super, расширяющее конструктор родительского класса и затем переопределяющее метод экземпляра describe, чтобы определить новое поведение. То же самое можно сделать в C#, но с иным синтаксисом:

using System;

public class Program
{
    public static void Main()
    {
        NobleGas argon = new NobleGas("Argon", "Ar", 18);
        argon.Describe();
    }
	
    public class Element
    {
        public string Name { get; set; }
        public string Symbol { get; set; }
        public int Number { get; set; }

        public Element(string name, string symbol, int number)
        {
            this.Name = name;
            this.Symbol = symbol;
            this.Number = number;
        }

        public virtual void Describe()
        {
            Console.WriteLine
            (
                "{0} ({1}) has atomic number {2}.",
                this.Name, this.Symbol, this.Number
            );
        }
    }
	
    public class NobleGas : Element
    {
        public string Category { get; set; }
        public string Type { get; set; }
        public string Reactivity { get; set; }
	    
        public NobleGas(string name, string symbol, int number) : base(name, symbol, number)
        {
            this.Category = "gas";
            this.Type = "noble gas";
            this.Reactivity = "low";
        }
	    
        public override void Describe()
        {
            Console.WriteLine
            (
                "{0} ({1}; {2}) is a {3} of type {4}. It has {5} reactivity.", 
                this.Name, this.Symbol, this.Number,
                this.Category, this.Type, this.Reactivity
            );
        }
    }
}

На первый взгляд, когда мы еще ничего не знали о C#, этот последний листинг мог показаться пугающим. Синтаксис незнакомый, какие-то странные ключевые слова и код организован не так, как мы привыкли. Однако, если рассмотреть этот код с точки зрения базовых принципов, разница не так существенна: здесь перед нами просто определение класса, набор методов и переменных и ряд правил для инстанцирования и использования объектов.

TL;DR


Изучать новый язык бывает адски сложно, если делать это с нуля. Однако, большинство языков программирования основаны на одних и тех же базовых принципах, между которыми легко провести параллели, подметить важные отличия и применять во множестве языков. Примеряя новый язык на уже сложившиеся ментальные модели, можно найти, где он не отличается от уже изученных языков, а где действительно требуется что-либо уточнить. Изучая со временем все новые и новые языки, вы расширяете ваши ментальные модели, и таких уточнений требуется все меньше и меньше – вы с листа распознаете разнообразные реализации на всевозможных языках.

Издательский дом «Питер»

242,00

Компания

Поделиться публикацией

Похожие публикации

Комментарии 21
    +1
    Используйте интерполяцию, глазки коровоточат.
    Тогда это:
    Console.WriteLine
                (
                    "{0} ({1}; {2}) is a {3} of type {4}. It has {5} reactivity.", 
                    this.Name, this.Symbol, this.Number,
                    this.Category, this.Type, this.Reactivity
                );

    превратится в это
    Console.WriteLine($"{this.Name} ({this.Symbol}; {this.Number}) is a {this.Category} of type {this.Type}. It has {this.Reactivity} reactivity.");
      +5
      Указание типа при объявлении переменной говорит о явной, а не о статической типизации.
        0
        +6
        Изучать новый язык бывает адски сложно
        Как-то неправильно поставлены акценты на мой взгляд. Синтаксис любого языка можно изучить за пару дней, особенно если это уже не первый язык. Гораздо важнее понять подкапотные различия, типа работа с памятью, сборка мусора, работа с процессами, и т.п. А если уж говорить о новичках на проекте, то по моему опыту гораздо больше времени занимает изучить незнакомые фреймворки и библиотеки, и еще больше — код, написанный ранее.
          –1
          PROLOG
          0
          Хочу посмотреть на такой же трюк где новый язык окажется не императивным, а логическим.
            +1

            Статью стоило назвать
            Осваиваем новые ООП языки программирования, опираясь на уже изученные ООП


            Но даже такое описание не полное.
            Как объяснить JavaScript программисту, что в С++/Java/C# нельзя передавать произвольное количество параметров произвольного типа за исключением крайних случаев?

              0
              printf считается крайним случаем?
                –3

                Исключения лишь подтверждают правило.

                  0

                  Не считается.


                  произвольного типа

                  А в ellipsis можно только POD-типы передавать.

                  +1
                  Гораздо сложнее наоборот объяснить C++ программисту, что this в JavaScript указывает не на текущий экземпляр, а на контекст.
                    0
                    Как объяснить JavaScript программисту, что в С++/Java/C# нельзя передавать произвольное количество параметров произвольного типа за исключением крайних случаев

                    Если сильно хочется, то все можно:
                    void MyFunc1(Object ... arg) {
                        ... arg[0]...arg[20]...
                    }
                    void MyFunc2(Map<String,Object> arg) {
                        ... arg.get("var1")... arg.get("var2")...
                    }
                    

                      +1

                      Первое — это что? Это не вариадик, и не эллипсис, и не массив.


                      Во втором случае у вас один аргумент, естественно.

                        0
                        Во втором случае у вас один аргумент, естественно.
                        К тому же и передавать его стоило бы по ссылке, а не по значению.
                  +2
                  Угу, угу…
                  Ты весь такой из себя Java разработчик, а тебе на и Haskell.
                    +1
                    ЧСХ с хаскеля на идрис потом примерно так же сложно. Хотя казалось бы…
                    0
                    Кажется, что чтобы изучить новый язык, иногда нужно наоборот «выключить» у себя в голове предыдущие.
                      0
                      Когда-то смотрел видос по руби для пхп-шников. Там чувак предлагал адаптироваться к руби методом замены, то бишь: меняем function на def, убираем { в начале методов, заменяем end в конце методов, убираем баксы и точки с запятыми… и вот вам грубо говоря руби :)
                        +1

                        Автору (не переводчику) следовало бы самому освоить языки о которых он пишет. Если вы делаете что то вроде:


                        List<String> particles = new List<String>();
                        particles.Add("electron");
                        particles.Add("proton");
                        particles.Add("neutron");

                        в шарпах, при этом пытаясь его преподавать Ruby-истам, это говорит как минимум о вашей некомпетентности. То же самое можно сказать о коде на Руби:


                        particles = ["electron", "proton", "neturon"]
                        particles.push("muon")
                        particles.push("photon")

                        Когда в том и другом языке есть просто элегантнейшие и ярчайшие конструкции для такого рода операций:


                        var product = new []{"electron"}.Union(new [] {"proton", "neutron"})

                        или


                        product = ["electron", "proton", "neturon"] + ["muon", "photon"]

                        Удивлен что автор в своих примерах обошелся без goto.

                          0
                          Статья ни о чём. Вот интересно, кто-нибудь что-нибудь новое или полезное из неё подчерпнул?

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

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

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