Сравнение C# и JavaScript. Основы

19 июня 2018 г.13 мин

Мой более-менее серьезный путь в программировании начался с написания программ на языке C#, иногда я пробовал писать на JavaScript, и то и дело впадал в ступор в таких ситуациях, когда неверно указывал имя переменной и узнавал об этом спустя много много лет час отладки, так как со мной рядом не было моего компилятора, который бы меня выручил в трудную минуту. Через некоторое время, помимо C# я начал писать много кода на JavaScript и в настоящее время могу делать это без особых трудностей, меня больше не смущает неявное приведение типов и динамическая типизация.

В данной статье я бы хотел систематизировать свои базовые знания об этих языках и рассмотреть их сходства и различия. Данная статья может служить руководством для C# разработчиков, которые хотят изучить JavaScript и наоборот. Также хочу заметить, что в данной статье описываются возможности клиентского JS, так как опыта разработки на Node.js у меня нет. Итак, если вы все ещё не потеряли интерес - приступим.

Namespace и js-модули

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

В C# для разбиения программы на части используются пространства имен. Для их объявления используется ключевое слово namespace. Например, если мы хотим создать набор компонентов пользовательского интерфейса, то логично поместить их все в одно пространство имен, например, Components. При этом принято чтобы пространство имен имело следующее именование [AssemblyName].[DirectoryName].[DirectoryName].[...]. В каждом файле класс компонента пользовательского интерфейса необходимо поместить внутрь пространства имен:

Содержимое файла ComboBox.cs:

namespace AssemblyName.Components
{
    public class ComboBox 
    { 
        // ...
    }
}

Для того, чтобы начать использовать компоненты необходимо импортировать их из пространства имён следующим образом using AssemblyName.Components. При данном способе подключения одной строкой мы импортируем все объекты в текущий файл.

В JS для этих же целей применяются ES-модули. При их использовании мы в какой-то степени эмулируем поведение пространств имен написанием дополнительного кода. Рассмотрим тот же пример с библиотекой компонентов. Допустим у нас есть папка Components, которая содержит компоненты пользовательского интерфейса ComboBox.js, Checkbox.js, Button.js и тд. Для того чтобы получить схожее поведение по сравнению с пространством имен в папке Components необходимо создать файл index.js, который будет содержать следующий код:

export { default as Dialog } from './ComboBox';
export { default as Button } from './Button';
export { default as Checkbox } from './Checkbox';
// ...

Для того, чтобы использовать данные компоненты необходимо импортировать их в текущий файл. Это можно сделать следующим образом: import * as Components from './../Components', после ключевого слова from нам необходимо указать путь к папке, в которой находятся все описанные компоненты.

Способы объявления переменных

Ключевое слово var

Как известно C# является строго типизированным языком программирования, поэтому при объявлении переменной компилятору должен быть известен её тип, для этого обычно он указывается перед её именем.

double pi = 3.14;
User user = new User();
int[] a = new[] { 0, 1, 2 };

Но мы также можем сказать компилятору, что он должен вывести тип самостоятельно из выражения, стоящего после знака присваивания. Это стало возможно благодаря введению в версии C# 3.0 ключевого слова var.

// i - int
var i = 5;
 
// a - int[]
var a = new[] { 0, 1, 2 };

С помощью var мы можем создавать объекты анонимного типа:

// anon - Анонимный тип только для чтения
var anon = new { Name = "Terry", Age = 34 };
var type = anon.GetType();//"<>f__AnonymousType0`2"

В JavaScript для объявления переменных также можно использовать ключевое слово var, однако, в отличии от C# областью видимости данных переменных будет вся функция или объект window, если переменная была объявлена вне функции.

var a = 5 // область видимости - window
 
function go() {
  var a = 6 // область видимости - функция go
 
  // ...
}

Хоть у вас и есть возможность объявлять переменные с помощью var в JavaScript, но сейчас этого делать не рекомендуется, после выхода стандарта ES6 было добавлено ключевое слово let, которое также позволяет объявлять переменные, но его преимуществом заключается в том, что их областью видимости будет являться блок, в котором они объявлены, а не вся функция.

Константы

Как в C#, так и в JavaScript для объявления константного поля используется ключевое слово const. Правда стоит отметить, что понятие константы в данном случае является различным для данных языков.

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

const int c1 = 5;
const int c2 = c1 + 100;
const string c3 = "Константа";
const bool c4 = true;
 
const User human = null;
const User human = new User(firstName); //недопустимо, ошибка компиляции

В JavaScript значение константы также нельзя изменять, однако нет ограничений, накладываемых на значение как в языке C#, ей можно присваивать любые значения/объекты/массивы. Однако, если в константу присвоен объект, то от изменения защищена сама константа, но не свойства внутри неё:

const c1 = 5;
const c2 = c1 + 100;
const c3 = "Константа";
const c4 = true;
 
const user = {
  name: "Петя"
};
 
user.name = "Петя"; // допустимо
user = 5; // нельзя, будет ошибка

Ключевое слово void

Во время написания данной статьи, я экспериментировал в консоли с функциями и по првычке начал описывать функцию как в C# void SomeFunction..., и для меня было большой неожиданностью, когда я узнал, что в JavaScript есть ключевое слово void. Как оказалось void в JavaScript является унарным оператором, который вычисляет значение операнда, затем отбрасывает его и возвращает undefined.

alert("Привет!");        // "Привет!"
alert(void "Привет!");   // undefined

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

В C# void не является оператором, однако по сути имеет схожее значение. Здесь он обозначает отсутствие возвращаемого значения функции:

public void SampleMethod()
{
    // ...
}

Однако, как можно заметить в примере выше, void стоит в том месте, где обычно указывается тип возвращаемого значения, и это не случайно, ведь в C# void также является типом.

var t = typeof(void);
t.Name // System.Void

voidв качестве типа может использоваться только в небезопасном контексте при работе с указателями.

unsafe
{
   void* identifier; //позволяется, но не рекомендуется
}

Ключевое слово new

В JavaScript ключевое слово new является оператором, и используется привычным для многих C-подобных языков образом - для создания объекта.

function Animal() {
    //...
}
 
const animal = new Animal();

В C# new может использоваться для следующих целей:

  • для создания объектов;
  • для скрытия наследуемого члена базового класса;
  • чтобы ограничить типы, которые могут использоваться в качестве аргументов для параметра типа в универсальном классе.

Первый случай аналогичен применению new в JavaScript.

class Animal
{
    //...
}
 
var animal = new Animal();

Основные типы данных

В каждом языке имеются типы данных - примитивы, на основе которых строятся другие типы данных, давайте рассмотрим типы данных предоставляемые нам в C# и JavaScript.

Примитивные типы С#:

  • Целочисленный со знаком: sbyte, short, int, long
  • Целочисленный без знака: byte, ushort, uint,ulong
  • Символы Unicode: char
  • Набор символов Unicode: char
  • Числа с плавающей запятой: float, double
  • Десятичный с повышенной точностью: decimal
  • Логическое значение: bool

Базовый классом является Object.

Для JS:

Примитивные типы данных:

  • Число number
  • Строка string
  • Логический тип boolean
  • Специальное значение null
  • Специальное значение undefined
  • symbol

Базовым типом является Object.

После изучения примитивов обоих языков можно придти к следующим выводам:

  • Вместо достаточно большого набора числовых типов в JavaScript имеется единственный тип number;
  • В JavaScript отсутствует тип char, вместо него стоит использовать тип string;
  • В обоих языках базовым типом является Object;
  • Отличительной особенностью JS является то, что null и undefined выделены в отдельные типы, в то время как в C# null - это ключевое слово обозначающее отсутствие значения.
  • В JS присутствует тип symbol, который используется в основном внутри самого стандарта JavaScript, для того чтобы иметь возможность добавлять новый функционал без конфликта с существующей кодовой базой.

Как правило, сейчас все больше приложений, в которых необходимо выполнять обработку данных на клиенте, для чего требуется большая точность в вычислениях. В настоящее время в JavaScript отсутствует встроенная возможность работы с большими числами, однако в недалеком будущем планируется добавить новый тип BigInt. Для решения аналогичных задач в C# имеется класс System.Numerics.BigInteger.

Проверка типа объекта

Проверка типа является достаточной типичной операцией для большинства языков программирования. Основываясь на типе, мы можем выполнять различные действия. Например, рассмотрим пример из жизни: вы слышите звонок в дверь, если к вам пришел пьяный сосед (объект с типом Пьяный Сосед) чтобы занять денег, то вы вряд ли откроете ему дверь, но если за дверью ваш лучший друг (объект с типом лучший друг), то вы не задумываясь впустите его в квартиру. C# и JavaScript также предоставляют средства для проверки типа объектов.

Оператор typeof

Для получения информации о типе как в C#, так и в JavaScript имеется оператор typeof. Давайте рассмотрим принцип его работы в обоих языках:

В С# оператор typeof применяется к типу и возвращает объект класса Type, который содержит всю информацию о типе.

namespace Zoo {
    public class Animal {}
}
 
Type t = typeof(Animal);
 
t.Name // 'Animal'
t.FullName // 'Zoo.Animall'
t.GetMethods // Информация о методах
t.GetFields // Информация обо всех полях
// ...

В JS typeof возвращает строку, указывающую тип операнда.

typeof 30 // 'number'
typeof Symbol() // 'symbol'
typeof undefined // 'undefined'
 
// Объекты
typeof new Animal() // object
typeof null // 'object'
typeof [1,2,3] // 'object'
 
// Функции
typeof function() {} // 'function';
typeof class C {} // 'function';

В примере выше можно заметить некоторые особенности работы данного оператора. Кажется логичным, если выражение typeof new Animal() возвращало бы строку 'Animal', a typeof [1,2,3] - строку Array, однако как бы ни было парадоксально, результатом в обоих случаях является 'object'. Также в связи с тем, что классы в JS являются оберткой над функциями, то выражение typeof class C {} вернет 'function' вместо 'class'. Ещё одним интересным фактом является то, что выражение typeof null вернёт 'object'. В JavaScript данный оператор имеет большой недостаток: все не примитивные объекты для него на одно лицо, все они имеют один тип object.

Стоит заметить, что в JavaScript typeof можно применять к чему угодно: объектам, функциям, классам и т.д.. В C# данный оператор применяется лишь к типам.

is и instanceof

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

В C# для данных целей имеется оператора is.

class Person
{
}
 
//Наследуем Programmer от Person
class Programmer : Person
{
}
 
var person = new Person();
var programmer = new Programmer();
 
person is Person //true
person is Programmer //false
programmer is Person //true
programmer is Programmer //true

В JavaScript для того, чтобы выяснить к какому типу принадлежит объект необходимо использовать оператор - instanceof.

function Person() {}
 
function Programmer() {}
//Наследуем Programmer от Person
Programmer.prototype = Object.create(Person.prototype);
 
var person = new Person();
var programmer = new Programmer();
 
console.log(person instanceof Person); // true
console.log(person instanceof Programmer); // false
console.log(programmer instanceof Person); // true
console.log(programmer instanceof Programmer); // true

Логические значения и проверка на null

Практически повсеместно, для того чтобы не получить Null reference exception, перед использованием переменной мы проверяем её на null, а в случае с JavaScript, ещё и на undefined.

В C# мы постоянно видим подобный код:

if(user != null && String.IsNullOrEmpty(user.name)) {
    user.SetName("Петя");
}

В JavaScript данную конструкцию можно записать несколько короче. Связано это с тем, что в отличии от C#, в JavaScript множество значений кроме false при приведении типов также расцениваются как false:

  1. null
  2. undefined
  3. «» (пустая строка)
  4. 0
  5. NaN (not a number)

Таким образом, приведенный выше код на C# можно записать следующим образом:

if (user && !user.name) {
    user.setName("Петя");
}

или

user && !user.name && user.setName("Петя");

В связи с тем, что проверки на null происходят повсеместно, в C# 6.0 был добавлен Null Propagation Operator .?.

Код на языке C#:

if (user != null && user.parent != null && user.parent.parent != null) {
    user.parent.parent.SetName("Петя");
}

С его помощью данный участок кода можно переписать следующим образом:

user?.parent?.parent?.SetName("Петя");

В JavaScript обычно делают следующим образом:

user && user.parent && user.parent.parent && user.parent.parent.setName("Петя");

Установка значений по-умолчанию

Ещё одной частой операцией является установка значений по-умолчанию, с версии 2.0 в C# появился Null Coalescing Operator - ??.

Следующие две строки кода на C# являются эквивалентными:

var name = user != null && user.name != null ? user.name : "Петя";
 
var name = user?.name ?? "Петя";

В JavaScript подобную операцию обычно делают следующим образом.

var name = user && user.name || "Петя";

Однако мы можем применять операторы && и || только в том случае, если 0, false и пустая строка не являются допустимыми значениями.

В обозримом будущем операторы ?., ?? должны появиться и в JavaScript (в настоящее время они прошли стадию Stage0), подробнее об этих операторах в JavaScript можно прочитать в статье.

Ключевое слово this

Как в C#, так и в JavaScript имеется ключевое слово this. Обычно в C# понимание предназначения this не вызывает никакого труда, однако в JavaScript это является одной из самых сложных концепций языка. Далее рассмотрим применение this на примерах.

В C# ключевое слово this указывает на текущий экземпляр класса.

class User
{
    public string Name { get; set; }
 
    public void PrintEmployee() {
        Console.WriteLine(this.name);
    }
}
 
var employee = new Employee();
E1.PrintEmployee();

В данном примере в выражении Console.WriteLine(this.name), this указывает на переменную employee.

Так как this - текущий экземпляр класса, то его нельзя использовать в методах не привязанных к определенному типу, например в статических методах.

В JavaScript значение this называется контекстом вызова и будет определено в момент вызова функции. Если одну и ту же функцию запускать в контексте разных объектов, она будет получать разный this:

var user = { firstName: "Петя" };
var admin = { firstName: "Админ" };
 
function func() {
  alert( this.firstName );
}
 
user.f = func;
admin.g = func;
 
// this равен объекту перед точкой:
user.f(); // Петя
admin.g(); // Админ
func();// undefined - в данном случае this - глобальный объект window

К тому же, в JavaScript присутствует возможность явного указания значения this с помощью функций: call, bind, apply. Например, вышеприведенный пример можно переписать следующим образом:

var user = { firstName: "Петя" };
var admin = { firstName: "Админ" };
 
function func() {
  alert( this.firstName );
}
 
// this равен объекту перед точкой:
func.call(user); // Петя
func.call(admin); // Админ
 
func.bind(user)();// Петя
func.bind(admin)();// Админ

Деструктуризация

Часто бывает необходимо присвоить несколько полей объекта локальным переменным. Например, как часто вы наблюдаете подобный код?

void Method(User user)
{
    var firstName = user.FirstName;
    var lastName = user.LastName;
    //...
}

Для подобных целей можно использовать деструктуризацию. Данную возможность в разной степени поддерживают оба языка.

В C# 7.0 для поддержки деструктуризации появился новый вид функций, называемый деконструкторами. Для того чтобы объявить деконструктор нам необходимо определить метод с именем Deconstruct, все параметры которого должны быть объявлены с модификатором out:

class Person
{
    public string FirstName { get; set; }
    public string LastName { get; set; }
 
    // Объявление деконструктора
    public void Deconstruct(out string firstName, out string lastName)
    {
        firstName = this.FirstName;
        lastName = this.LastName;
    }
}
 
...
 
Person person = new Person { FirstName = "Петя", LastName = "Петров" };
 
(string firstName, string lastName) = person;
(string firstName, _ ) = person;

Поддержка деструктуризации или (destructuring assignment) в JavaScript появилась в шестом стандарте EcmaScript. С её помощь. можно присвоить массив или объект сразу нескольким переменным, разбив его на части.

let [firstName, lastName] = ["Петя", "Петров"];
let [firstName, _ ] = ["Петя", "Петров"];
 
let { firstName, lastName } = { firstName: "Петя", lastName: "Петров" };
let { firstName } = { firstName: "Петя", lastName: "Петров" };

Стоит отметить, что деструктуризация в JavaScript имеет больше возможностей, чем в C#:

  • Изменение порядка переменных;
  • Отсутствие необходимости явного объявления деконструкторов;
  • Поддержка деструктуризации массивов;
  • Установки значений по-умолчанию;
  • Присваивание свойств объекта в переменную с другим именем;
  • Поддержка вложенной деструктуризации.

Заключение

В данной статье мы обсудили лишь самые основные концепции языков C# и JavaScript. Но остались не затронуты ещё многие аспекты:

  • коллекции
  • функции
  • классы
  • многопоточность

Каждая из этих тем является достаточно обширной и будет раскрыта в дальнейшем в отдельной статье.