Вопросы по JavaScript и C#, которые неожиданно вызывают затруднения на собеседованиях

13 августа 2014, 16:20

За последние полгода наша команда провела более сорока технических интервью с кандидатами на позиции front-end разработчика (JavaScript/CSS/ASP.NET MVC) и .NET-разработчика (С#/WPF). Мы не используем тесты и тестовые задания, а просим написать несколько строчек кода и найти ошибку в выражении. Такая методика позволяет оценить не только память, но и мышление, способности к поиску альтернативных решений и "техническое чутьё" всего за полтора часа.

В данной статье мы собрали задания, которые вызывают сложности у кандидатов разного уровня подготовки. Давайте посмотрим, какими знаниями нужно обладать, чтобы успешно пройти собеседование на должность front-end разработчика или программиста .NET в Минском офисе компании IHS.

Вопросы по JavaScript и С#/.NET

JavaScript

Наше интервью по JavaScript не проходит без вопросов о типах. Например:

1. Можно ли записать такие выражения на JavaScript? Если да, то какое значение будет у переменной a?

var a = +!!{};
var a = -![];

2. Какие значения относят к значениям falsy?

3. Чем отличается null от undefined?

4. Чему равна переменная а и какого она типа?

var a = 2 + "2";

5. Покажется ли диалоговое окно?

var a = "";
if (a != 0)
{
	alert(a);
}

А если так?

var a = "";
if (a !== 0)
{
	alert(a);
}

На самом деле можно придумать ещё много подобных вопросов, целью которых является проверка знаний особенностей типов и их приведения в JavaScript. Чтобы ответить успешно на все вопросы о типах в JavaScript нужно знать следующее. 

JavaScript – это интерпретируемый язык программирования c динамической типизацией и автоматическим управлением памятью. Свойство динамической типизации языка означает то, что переменной одного типа можно присвоить значение другого типа (в отличие от языков статической типизации – С++, С#, Java). Переменные всегда имеют конкретный тип, который определяется оператором typeof.

Встроенные типы значений в JavaScript (источник)

Тип Описание
number Числовой тип c плавающей запятой, 8 байт, аналог double (источник)
boolean Булев тип, содержит только 2 значения: true и false
string Строковый тип, символ занимает 2 байта, кодировка UTF-16 (источник)
object Любой встроенный или пользовательский объект, включая массив и объект даты
function Любой объект функции
undefined Специальное значение для неинициализированной переменной

Сравните значения, возвращаемые этим оператором, в консольном примере, запущенном на Node.js. 

var a;
console.log(typeof(a));
a = "String value";
console.log(typeof(a));
a = 5;
console.log(typeof(a));
a = false;
console.log(typeof(a));
a = function() { };
console.log(typeof(a));
a = {};
console.log(typeof(a));
e:\Articles>node .\typeof.js
undefined
string
number
boolean
function
object

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

Помимо типов в JavaScript существует понятие truthy и falsy значений (подробнее во второй главе книги "Object-Oriented JavaScript" Стояна Стефанова). Они примечательны тем, что в условиях конструкций if-elsefor, и while значения truthy ведут себя как значение true булева типа, а значения falsy – как значение false булева типа.

К значениям falsy относятся значения, перечисленные ниже.

Falsy значение Описание
"" или '' Пустая строка, тип string
0 Ноль, тип number
null Переменная, инициализированная пустым значением, тип object
undefined Неинициализированная переменная, тип undefined
false Само значение false типа boolean
NaN Нечисло, тип number

К значениям truthy относятся все остальные.

Значения falsy ведут себя как false булева типа и в операциях нестрогого сравнения (== и !=, но только при сравнении между false, 0 и пустой строкой).

Введя понятия truthy и falsy, можно рассмотреть операцию приведения к типу boolean. Для этого существует оператор отрицания: ! – одинарное отрицание, !! – двойное отрицание.

Одинарное отрицание (!) приводит значение truthy к false, а значение falsy к true.

Двойное отрицание (!!) приводит значение truthy к true, а значение falsy к false.

На Node.js все вышесказанное выглядит следующим образом.

console.log("!false = " + (!false));
console.log("!true = " + (!true));
console.log("!{} = " + (!{}));
console.log("![] = " + (![]));
console.log("!0 = " + (!0));
console.log("!null = " + (!null));
console.log('!function(){}='+(!function(){}));
console.log("!'' = " + (!''));
console.log("!'a' = " + (!'a'));
console.log("!'NaN' = " + (!(0/0)));
console.log("!undefined = " + (!void(0)));

console.log("!!false = " + (!!false));
console.log("!!true = " + (!!true));
console.log("!!{} = " + (!!{}));
console.log("!![] = " + (!![]));
console.log("!!0 = " + (!!0));
console.log("!!null = " + (!!null));
console.log('!!function(){}='+(!!function(){}));
console.log("!!'' = " + (!!''));
console.log("!!'a' = " + (!!'a'));
console.log("!!'NaN' = " + (!!(0/0)));
console.log("!!undefined = " + (!!void(0)));
e:\Articles\node .\boolean.js
!false = true
!true = false
!{} = false
![] = false
!0 = true
!null = true
!function() {} = false
!'' = true
!'a' = false
!'NaN' = true
!undefined = true
!!false = false
!!true = true
!!{} = true
!![] = true
!!0 = false
!!null = false
!!function() {} = true
!!'' = false
!!'a' = true
!!'NaN' = false
!!undefined = false

В JavaScript есть два оператора сложения (+): унарный и бинарный. Унарный + требует лишь один операнд и выполняет приведение типа к числу. Бинарный + служит для конкатенации строк, если хотя бы один операнд не является числом или булевым значением, или для сложения чисел, если оба операнда типа number или boolean.

Правило работы унарного оператора + (подробнее): значение falsy приводится к 0 типа number, значение truthy кроме строк приводится к нечислу (NaN) типа number, строка, содержащая число, приводится к этому числу типа number, любая другая строка даёт в результате нечисло (NaN) типа number, значение true приводится к 1.

console.log("+0 = " + (+0));
console.log("+false = " + (+false));
console.log("+true = " + (+true));
console.log("+{} = " + (+{}));
console.log("+[] = " + (+[]));
console.log("+[1,2,3] = " + (+[1,2,3]));
console.log("+'' = " + (+''));
console.log("+NaN = " + (+(0 / 0)));
console.log("+null = " + (+null));
console.log("+undefined = " + (+void(0)));
console.log("+'45' = " + (+'45'));
console.log("+'a' = " + (+'a'));
console.log("+'45a' = " + (+'45a'));
console.log("+'a45' = " + (+'a45'));
e:\Articles>node .\number.js
+0 = 0
+false = 0
+true = 1
+{} = NaN
+[] = 0
+[1,2,3] = NaN
+'' = 0
+NaN = NaN
+null = 0
+undefined = NaN
+'45' = 45
+'a' = NaN
+'45a' = NaN
+'a45' = NaN

Обращаем внимание, что значение undefined приводится к нечислу (NaN) по спецификации (источник). Обращаем также внимание на то, что приведение пустого массива к типу number даёт 0. Это из-за внутренней реализации метода DefaultValue массива, который возвращает для пустого массива пустую строку, а для массива с одним элементом – значение этого элемента (источник).

Помимо унарного оператора + в JavaScript имеется и унарный минус (-), действие которого аналогично унарному оператору + с той лишь разницей, что результат операции приведения типа является отрицательным числом (подробнее).

Бинарный оператор + переходит в режим конкатенации строк, если хотя бы один операнд не является числом или булевым значением. Приведение операнда к строке выполняется через вызов метода toString.

Если бинарный + применяется к числу и логическому значению, то последнее приводится к типу number. То же самое происходит и при сложении двух логических значений, поэтому true + true === 2.

Теперь дать ответы на вопросы, заданные в начале раздела, не составляет труда:

1. Результат первого выражения – число 1, второго – 0 (вернее 0 с минусом, что соответствует спецификации).

2. Согласно Стояну Стефанову: false, null, undefined, NaN, "" (пустая строка) и 0.

3. Типом и назначением.

4. Строка "22".

5. В первом случае диалог не покажется, т.к. при нестрогом сравнении пустая строка признаётся равной нулю. То же самое верно и при нестрогом сравнении с false, но неверно при сравнении с null, undefined и NaN (нечисло не равно даже самому себе).

С#/.NET

Трудно представить приложение, в котором не требуется работа со строками. Интернирование и неизменность строк в среде .NET является избитой темой, однако практика показывает, что не все кандидаты имеют чёткое представление об этих свойствах. Нужно пояснить, что интернирование строк встречается не только в .NET, но и в Java, а также других средах программирования (подробнее). Давайте разберёмся, что необходимо знать, чтобы легко ответить на следующие вопросы:

1. Почему строки в .NET являются неизменяемыми объектами (immutable)?

2. Что такое интернирование строк в .NET и можно ли отключить это поведение?

3. Почему для конкатенации строк лучше использовать StringBuilder, а не String.Concat?

Интернирование строк – это метод организации экземпляров строк в памяти выполняемого приложения, при котором для каждого строкового значения создается (или хранится) только один объект строки.

В следующем примере двум переменным присваивается одно и то же строковое значение. Если мы запустим этот код на выполнение, то увидим, что обе переменные указывают на один и тот же объект в памяти.

class Program
{
    static void Main(string[] args)
    {
        var a = "test string";
        var b = "test string";            
        // выводит True
        Console.WriteLine("Строковые литералы интернированы: " + ReferenceEquals(a, b));
    }
}

Структура данных для хранения интернированных строк называется Intern Pool.

По умолчанию все литеральные строки в .NET интернированы, а строки, создаваемые динамически, – нет. Однако в классе System.String есть статический метод Intern, который позволяет поместить объект строки в Intern Pool, если строка ещё не интернирована, и вернуть ссылку на интернированный объект (подробнее).

Основные преимущества интернированных строк: уменьшение объёма памяти для строковых объектов и увеличение производительности операций сравнения. Методы Equals класса System.String сначала проверяют равенство строк по ссылке, а затем – по значению.

Реализация метода Equals, дизассемблированного ILSpy

public static bool Equals(string a, string b)
{
    return a == b || (a != null && b != null && a.Length == b.Length && string.EqualsHelper(a, b));
}

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

class Program
{
    static void Main(string[] args)
    {
        var s1 = new string(new[] { 't','e','s','t',' ','s','t','r','i','n','g' });
        var s2 = string.Join(" ", new[] { "test", "string" });

        Console.WriteLine(ReferenceEquals(s1, s2));

        var s1Interned = string.Intern(
            new string(new [] { 't','e','s','t',' ','s','t','r','i','n','g' }));

        var s2Interned = string.Intern(string.Join(" ", new [] { "test", "string" }));

        Console.WriteLine(ReferenceEquals(s1Interned, s2Interned));
        
        var n = 10000000;
        var sw = new Stopwatch();
        sw.Start();
        for (int i = 0; i < n; i++)
        {
            var res = s1 == s2;
        }
        sw.Stop();
        Console.WriteLine("Сравнение неинтернированных: " + sw.ElapsedMilliseconds);
        sw.Reset();
        sw.Start();
        for (int i = 0; i < n; i++)
        {
            var res = s1Interned == s2Interned;
        }
        sw.Stop();
        Console.WriteLine("Сравнение интернированных: " + sw.ElapsedMilliseconds);
        sw.Reset();

        sw.Start();
        for (int i = 0; i < n; i++)
        {
            switch (s1)
            {
                case "test string": break;
            }
        }
        sw.Stop();
        Console.WriteLine("Сравнение неинтернированных [switch]: " + sw.ElapsedMilliseconds);
        sw.Reset();
        sw.Start();
        for (int i = 0; i < n; i++)
        {
            switch (s1Interned)
            {
                case "test string": break;
            }
        }
        sw.Stop();
        Console.WriteLine("Сравнение интернированных [switch]: " + sw.ElapsedMilliseconds);

        Console.ReadKey();
    }
}

Полученный результат:

False
True
Сравнение неинтернированных: 144
Сравнение интернированных: 48
Сравнение неинтернированных [switch]: 148
Сравнение интернированных [switch]: 56

Вывод: если в памяти необходимо хранить большой объём строковых данных с повторяющимися значениями для последующего поиска (кэш), то имеет смысл работать именно с интернированными строками.

Стоит отметить, что в .NET интернирование строк реализовано в пределах домена приложения.

Реализация метода Intern, дизассемблированного ILSpy

public static string Intern(string str)
{
    return Thread.GetDomain().GetOrInternString(str);
}

Чтобы интернирование было возможно, строка должна быть неизменной (immutable). Это значит, что любая операция, модифицирующая значение строки, порождает новый экземпляр класса System.String, совершенно не затрагивая существующий. Однако это не эффективно при частых операциях объединения и замещения частей строк, т.к. приводит к созданию слишком большого числа экземпляров класса System.String, которые содержат промежуточные строковые значения. Для решения этой проблемы служит класс StringBuilder. Принцип его работы прост: исходная строка преобразуется в массив символов, далее все операции по изменению значений производятся уже над массивом символов. Для получения результирующей строки используется метод ToString, который вначале выделяет необходимую память, а затем посимвольно копирует массив с помощью внутреннего метода String.wstrcpy по новому адресу. StringBuilder используется внутри метода String.Join.

Оператор сложения строк + аналогичен String.Concat. С помощью ILDasm можно убедиться, что они компилируются в один и тот же IL-код.

class Program
{
  static void Main(string[] args)
  {
    string s1 = "test";
    string s2 = "string";
    string s = s1 + s2;
  }
}

 

class Program
{
  static void Main(string[] args)
  {
    string s1 = "test";
    string s2 = "string";
    string s = string.Concat(s1, s2);
  }
}

 

.method private hidebysig static void  Main(string[] args) cil managed
{
  .entrypoint
  // Code size       21 (0x15)
  .maxstack  2
  .locals init ([0] string s1,
           [1] string s2)
  IL_0000:  ldstr      "test"
  IL_0005:  stloc.0
  IL_0006:  ldstr      "string"
  IL_000b:  stloc.1
  IL_000c:  ldloc.0
  IL_000d:  ldloc.1
  IL_000e:  call       string [mscorlib]System.String::Concat(string,
                                                              string)
  IL_0013:  pop
  IL_0014:  ret
} // end of method Program::Main

Вывод: для объединения множества строк нужно использовать StringBuilder или String.Join, для выполнения множества операций замещения частей строки нужно использовать StringBuilder.Replace, а не String.Replace.

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

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

Заключение

Мы привели примеры вопросов по .NET, которые проверяют знания в областях, понимание которых необходимо для решения задач построения нагруженных систем, работающих с большими объёмами данных. Акцент здесь делается не только на владении технологиями, но и на понимании того, как они реализованы. Такие знания требуются для повышения качества и оптимизации продукта, а также для поиска альтернативных архитектурных решений.

Знание особенностей типов и их приведения в JavaScript позволяет создавать лаконичный и эффективный код. Пример: функция isNumeric в jQuery:

isNumeric: function( obj ) {
  // parseFloat NaNs numeric-cast false positives (null|true|false|"")
  // ...but misinterprets leading-number strings, particularly hex literals ("0x...")
  // subtraction forces infinities to NaN
  // adding 1 corrects loss of precision from parseFloat (#15100)
  return !jQuery.isArray( obj ) && (obj - parseFloat( obj ) + 1) >= 0;
},

Другим отличным примером является функция, которую можно использовать для форматирования даты (первая глава книги Test-Driven JavaScript Development):

function zeroPad(num) {
  return (+num < 10 ? "0" : "") + num;
}

На сегодня всё. Хотелось бы узнать мнение читателей о необходимости делиться подобными материалами.

подписка на главные новости 
недели != спам
# ит-новости
# анонсы событий
# вакансии
Обсуждение