Топ 10 вопросов на iOS собеседовании Senior разработчика. Часть 2

Данная статья является продолжением Топ 10 вопросов на iOS собеседовании. Уровень Senior. Часть 1. Мы продолжим разбирать популярные вопросы, встречающиеся на iOS собеседовании уровня Senior или выше. Все вопросы взяты из реальных собеседований.

1. Посмотрите на код ниже. Будут ли равны результаты? Поясните ответ

NSString *firstName = @"User Name";
NSString *secondName = @"User Name";

if (firstName == secondName) {
    NSLog(@"равны");
} else {
    NSLog(@"не равны");
}
let firstName: NSString = "User Name"
let secondName: NSString = "User Name"

if firstName === secondName {
    print("равны")
} else {
    print("не равны")
}

Начнем с первого примера на Objective-C.

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

Во-вторых, давайте посмотрим что происходит с адресом памяти. Напишем небольшой пример:

NSString *firstName = @"User Name";
NSString *secondName = firstName;
NSLog(@"%p %p", firstName, secondName);

Так как объект firstName присваивается по ссылке, то конечно же в консоли распечатаются два одинаковых адреса в памяти.

Если изменить код следующим образом:

NSString *firstName = @"User Name";
NSString *secondName = @"User Name";
NSLog(@"%p %p", firstName, secondName);

То адреса в памяти снова будут одинаковые!

Давайте экспериментировать дальше. Создадим два массива с одинаковыми значениями:

NSArray *firstArray = @[@"User Name"];
NSArray *secondArray = @[@"User Name"];
NSLog(@"%p %p", firstArray, secondArray);

В этот раз адреса в памяти будут разные, поскольку мы создали два объекта NSArray по отдельности. Но почему это не работает с NSString?

На самом деле эти трюки объясняются попытками компилятора оптимизировать строковые ресурсы. Если компилятор видит две строки с одинаковым содержимым, то для экономии места он создает всего один адрес в памяти. Это безопасно, ведь NSString — неизменяемая строка. Заметили сходство с механизмом Copy-on-Write?

Исправить ситуацию можно, например, использовав метод stringWithFormat для создания строки:

NSString *firstName = @"User Name";
NSString *secondName = [NSString stringWithFormat:@"%@", firstName];
NSLog(@"%p %p", firstName, secondName);

Итак, с первым примером разобрались — ответ будет «равны».

Со втором примером — аналогично. Если написать небольшой код для проверки:

let firstName: NSString = "User Name"
let secondName: NSString = "User Name"

print(String(format: "%p", firstName)) // 0xa27680fbdf61ebfc
print(String(format: "%p", secondName)) // 0xa27680fbdf61ebfc

То можно заметить одинаковые адреса в памяти для объектов firstName и secondName. Обратите внимание, что в примере с языком Swift используется проверка на тождественность.

Конечный ответ на вопрос: в первом и во втором примерах результат будет одинаковый.

Что еще почитать:

2. Посмотрите на код ниже. Есть ли в нем проблемы? Если есть, как исправить?

class A {
    lazy var classB: B = B(classA: self)
    
    init() {
        print("A init")
    }
    
    deinit {
        print("A deinit")
    }
}

class B {
    var classA: A
    
    init(classA: A) {
        print("B init")
        self.classA = classA
    }
    
    deinit {
        print("B deinit")
    }
}

func createClasses() {
    let classA = A()
    let classB = classA.classB
}

createClasses()

Да, проблемы в этом коде есть. Он запустится и будет работать. Но произойдет удержание ссылок (retain cycle):

  • Класс A держит сильную ссылку на класс B
  • Класс B, в свою очередь, держит сильную ссылку на А
  • Начинается замыкание со строчки let classB = classA.classB, когда и происходит инициализация класса B (обратите внимание на lazy).

Если запустить программу, то выведется следующий результат:

A init
B init

то есть можно увидеть, что deinit не сработал.

Как можно исправить ситуацию? Есть два выхода:

  1. Удалить или закомментировать строчку let classB = classA.classB, тогда lazy переменная не будет проинициализирована и не произойдет удержание сильных ссылок
  2. В классе B изменить объявление переменной classA на следующее: weak var classA: A?. Таким образом мы избавляемся от проблемы retain cycle.

Если реализовать второй вариант, то при запуске программы получим следующий результат:

A init
B init
A deinit
B deinit

3. Посмотрите на код ниже. Что будет выведено в результате выполнения кода?

final class CustomArray {
    var array: [Int] = [] {
        didSet {
            print("didSet: \(array)")
        }
    }
    
    init(_ array: [Int]) {
        self.array = array
    }
    
    deinit {
        print("deinit")
        array = [4]
    }
}

// 1
var customArray: CustomArray? = CustomArray([1])
// 2
customArray?.array = [2]
// 3
customArray?.array.append(3)
// 4
customArray = nil

Похожие вопросы на собеседовании за последний год попадались около трех раз.

  1. При инициализации объекта CustomArray значением [1] наблюдатели свойств willSet() и didSet() не вызываются, поэтому на первом шаге ничего не выводится.
  2. На втором шаге мы инициализируем массив значением [2]. В этот момент сработает наблюдатель свойства didSet()
  3. На третьем шаге мы снова делаем изменения в нашем массиве — добавляем новое значение в массив. Сработает didSet().
  4. На последнем шаге мы уничтожаем объект, сработает deinit(). В методе deinit также происходит инициализация массива значением [4], но так как deinit вызывается по мере выхода из функции/класса, то фактически массив будет проинициализирован новым значением, но наблюдатель didSet уже не будет вызван (этот момент практически нигде не задокументирован).

Делаем выводы: наблюдатели свойств не вызываются при инициализации/деинициализации. Получаем ответ:

didSet: [2]
didSet: [2, 3]
deinit

Дополнительное чтение:

4. Посмотрите на код ниже. Измените его таким образом, чтобы после выполнения программы значение count равнялось 400?

var count = 0

let thread1 = Thread {
    for _ in 0...199 {
        count += 1
    }
}

let thread2 = Thread {
    for _ in 0...199 {
        count += 1
    }
}

thread1.start()
thread2.start()

Этот код мы уже разбирали в прошлой статье: Топ 10 вопросов на iOS собеседовании. Уровень Senior. Часть 1

На самом деле есть несколько вариантов исправить код, давайте приведем один из них (спасибо Роману за замечание в комментариях) — через NSLock():

var count = 0
let lock = NSLock()

let thread1 = Thread {
    for _ in 0...199 {
        lock.lock()
        count += 1
        lock.unlock()
    }
}

let thread2 = Thread {
    for _ in 0...199 {
        lock.lock()
        count += 1
        lock.unlock()
    }
}

thread1.start()
thread2.start()

После выполнения программы значение count будет гарантированно рано 400.

5. Посмотрите на код ниже. Что выведется после выполнения программы?

protocol P { }

extension P {
    func method() {
        print("from protocol")
    }
}

struct C: P {
    func method() {
        print("from class")
    }
}

let firstObject = C()
firstObject.method()

let secondObject: P = C()
secondObject.method()

Похожие вопросы на собеседовании встречаются часто, но в разных вариациях. Для ответа вам нужно хорошо разбираться в диспетчеризации методов. Тему диспетчеризации немного затрагивали в статье Топ 20 вопросов на iOS собеседовании. Уровень Middle.

  • При работе с firstObject выведется «from class«, так как метод method() объявлен внутри структуры — статическая диспетчеризация.
  • При работе с secondObject выведется «from protocol«, так как метод method() объявлен в расширении к протоколу — статическая диспетчеризация

Чтобы закрепить данную тему, попробуйте ответить на вопрос: Почему при изменении кода следующим образом выводится «from class»? (объявлен метод в протоколе):

protocol P {
    func method()
}

extension P {
    func method() {
        print("from protocol")
    }
}

struct C: P {
    func method() {
        print("from class")
    }
}

let firstObject = C()
firstObject.method() // from class

let secondObject: P = C()
secondObject.method() // from class

Присылайте свои ответы в комментариях.

6. Что такое Responder Chain?

Если кратко, то Responder Chain — это иерархия объектов, которые могут ответить на полученные события.

Для обработки взаимодействия пользователя с UI и внешних событий в iOS используется механизм Responder Chain.

Класс UIApplication, UIViewController и UIView наследуются от класса UIResponder.
Класс UIResponder определяет порядок, в котором объекты обрабатывают события (touch-события, события от элементов UI (кнопки и т.д.), изменение текста).

Кроме того, UIResponder объявляет методы, которые позволяют объектам определять, кто первым будет отвечать и обрабатывать сообщения:

  • becomeFirstResponder — получатель сообщения будет первым получать все события, посылаемые системой.
  • resignFirstResponder — получатель отказывается от обработки сообщений первым.

Дополнительное чтение:

7. Посмотрите на код ниже. Что выведется после выполнения программы?

var array = [1,2,3]
for i in array {
    print(i)
    array = [4,5,6]
}

Ответ на этот вопрос будет: 1 2 3. Цикл for захватывает массив array, поэтому никакие изменения этого массива внутри цикла не изменят конечного результата.

8. Можно ли отменить операцию на выполнении? (В GCD и NSOperation)

В NSOperation мы имеем возможность отменить операции, в GCD — нет.

Дополнительное чтение:

Разница между NSOperation и Grand Central Dispatch

9. Какая диспетчеризация используется в приведенном примере?

class Person: NSObject {
    func sayHi() {
        print("Hello")
    }
}

func greetings(person: Person) {
    person.sayHi()
}

greetings(person: Person())

На собеседованиях Middle+ и выше очень часто спрашивают про диспетчеризацию, если на уровне Middle спрашивают типы диспетчеризации, то на уровне Senior могут попросить определить тип.

В данном случае используется табличная диспетчеризация.

10. За какое количество итераций в худшем случае вы сможете найти элемент в массиве из 32 объектов, используя алгоритм бинарного поиска?

Для ответа на этот вопрос вам нужно хотя бы немного разбираться в алгоритмах. Я уже писал как можно подтянуть алгоритмы для прохождения собеседований. Чаще всего алгоритмы спрашивают на уровнях Middle — Senior.

Так как мы используем бинарный поиск (делим пополам наш массив и если искомое значение больше или меньше среднего значения, то отсекаем либо правую часть массива, либо левую), то его сложность равна O(log n). Подставляем наше значение и получаем результат: log 32 = 5.

Ответ: в худшем случае за 5 итераций.

бинарный поиск - ios-interview

Дополнительное чтение:

Где еще посмотреть вопросы на собеседовании?

Выразить благодарность или найти уникальный материал вы можете в boosty.

Подписывайтесь на мой Telegram-канал iOS Interview Channel, чтобы не пропустить новый материал.


3 thoughts on “Топ 10 вопросов на iOS собеседовании Senior разработчика. Часть 2”

  1. В ответе на вопрос 4 блокируется цикл — в таком случае смысл создания потоков продает.
    Логичнее лочить во время изменения переменной:

    for _ in 0...199 {
    lock.lock()
    count += 1
    lock.unlock()
    }

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *