Sparround

Rapid-fire suallar və live coding

Flutter developer interview-larının texniki hissəsi demək olar ki, həmişə eyni iki formatdan başlayır: rapid-fire (10-20 qısa Dart sualı, hər birinə 20-30 saniyə) və live coding (bir kiçik məsələ, 20-40 dəqiqə).

Rapid-fire-ın məqsədi dərinlik yoxlamaq deyil — əsasların oturuşub-oturuşmadığını görməkdir. Ona görə uzun cavab burada mənfi siqnaldır: sualın sadə olduğunu görmədiyini bildirir.

Üç hissəli cavab düsturu — hər qısa suala bu şablonla cavab ver:

1. Birbaşa cavab (bir cümlə): "const compile-time sabitdir, final isə yalnız bir dəfə mənimsədilə bilən dəyişəndir." 2. Bir sətir "niyə" (mexanizm): "...ona görə const obyektlər kompilyasiya zamanı yaradılıb canonicalize olunur." 3. Bir konkret nümunə: "...məsələn eyni arqumentlərlə iki const Money(100, 'AZN')identical onlar üçün true qaytarır."

Bu düstur 20-30 saniyəyə yerləşir və üç şeyi eyni anda göstərir: bilirsən, başa düşürsən, işlətmisən.

Bilmirsənsə: "Bunu dəqiq bilmirəm. Güman edirəm ki..., amma yoxlayaram" — normal və hörmətli cavabdır. 15 sualdan 2-3-nə belə cavab vermək gözləniləndir. Uydurmaq isə ən pis variantdır: intervyuer bunu demək olar ki, həmişə hiss edir və ondan sonra bütün cavablarına şübhə ilə baxır.

SualModel cavab (20-30 saniyə)
`const` ilə `final` fərqi?`final` — bir dəfə mənimsədilir, dəyəri runtime-da hesablana bilər (`final now = DateTime.now()`). `const` — compile-time-da tam məlum olmalıdır və dərin immutable-dır; eyni arqumentli `const` obyektlər canonicalize olunur, yəni `identical` onlar üçün `true` verir.
`?.` ilə `!` fərqi?`?.` — null olarsa, çağırışı atlayır və `null` qaytarır. `!` — "bu null deyil" deyə kompilyatora verdiyin vəddir; yanılırsansa, runtime-da xəta atılır. `?.` təhlükəsiz, `!` isə sübut tələb edir.
`late` nə üçündür?İki iş üçün: non-nullable sahəni konstruktordan sonra doldurmaq və lazy hesablama (`late final x = expensive()` — ilk müraciətdə hesablanır, sonra keşlənir). Qiyməti: kompilyator yoxlaması runtime `LateInitializationError` riskinə çevrilir.
`sealed` sinif ilə `enum` fərqi?`enum` — sabit **dəyərlər** dəsti; `sealed` — sabit **alt tiplər** dəsti, hər biri öz sahələri ilə. Hər ikisində `switch` exhaustive olur, amma `sealed` data daşıya bilir: `Loading`, `Success(data)`, `Failure(error)` — bunu enum ilə yaza bilməzsən.
`mixin` ilə interfeys fərqi?`mixin` implementasiya verir — `with` ilə qoşursan və metodları hazır alırsan. `implements` yalnız kontrakt qoyur: hər üzvü özün yazmalısan. Dart-da hər sinif həm də implicit interfeysdir, ona görə `implements SomeClass` mümkündür. Miras isə birdir: `extends` yalnız bir sinifdən.
`abstract class` nə vaxt, `interface class` nə vaxt?`abstract class` — ümumi kod və vəziyyət paylaşmaq istəyəndə (birbaşa instansiyalaşdırıla bilmir). Dart 3-ün `interface class` modifikatoru isə kitabxanadan kənarda `extends` etməyi qadağan edir: yalnız `implements`. Bu, API-ni gələcək dəyişikliklərə qarşı qoruyur.
Record ilə sinif fərqi?Record — adsız, yüngül dəyər dəsti: `(String, int)` və ya `({String name, int age})`. Strukturlu bərabərliyi hazır gəlir, `==` və `hashCode` yazmaq lazım deyil. Sinif isə ad, davranış (metodlar) və invariantlar (validasiya) verir. Qaydam: funksiyadan iki dəyər qaytarmaq üçün record, domain anlayışı üçün sinif.
`==` override edəndə nəyi unutmaq olmaz?`hashCode`-u. Qayda: bərabər obyektlərin hash-i mütləq bərabər olmalıdır; əks halda obyekt `Set`-də dublikat kimi görünür və `Map` açarı kimi tapılmır. `Object.hash(a, b)` və ya kolleksiya üçün `Object.hashAll(...)` istifadə edirəm, sahələr isə `final` olur.
Extension metodu necə həll olunur?**Statik** olaraq, dəyişənin **statik tipinə** görə, kompilyasiya zamanı. Ona görə `dynamic` üzərində extension işləmir və override oluna bilmir — bu, adi metod dispatch-ından əsas fərqdir. İki extension eyni adı verirsə, `MyExt(obj).method()` ilə açıq seçim edirsən.
`dynamic` ilə `Object?` fərqi?Hər ikisi istənilən dəyəri saxlayır, amma `Object?` tip yoxlamasını saxlayır — yalnız `Object` üzvlərini çağıra bilərsən, qalanı üçün cast və ya pattern lazımdır. `dynamic` isə yoxlamanı **söndürür**: hər çağırış kompilyasiya olunur və runtime-da partlaya bilər. Default seçim `Object?`.
Dart-da generiklər variant baxımından necədir?Kovariantdır: `List<int>` həm də `List<num>` sayılır. Bu, oxumaq üçün rahatdır, amma yazmaq üçün təhlükəlidir — `List<num>` kimi tutduğun `List<int>`-ə `1.5` əlavə etsən, runtime-da `TypeError` alırsan. Ona görə ümumi API-lərdə oxunan parametrləri `Iterable<T>` kimi qəbul edirəm.
SualModel cavab (20-30 saniyə)
`Future` ilə `Stream` fərqi?`Future` — gələcəkdə **bir** nəticə (və ya bir xəta), `await` ilə alınır. `Stream` — zamanla **sıfır və ya çox** hadisə, `listen` və ya `await for` ilə. Sadə qayda: HTTP sorğusu Future, WebSocket və ya axtarış sahəsinin dəyişmələri Stream.
Microtask queue ilə event queue fərqi?Event loop əvvəlcə microtask növbəsini **tam** boşaldır, sonra event növbəsindən bir hadisə götürür. Future-ların callback-ləri microtask-a, `Timer`, I/O və istifadəçi hadisələri isə event növbəsinə düşür. Praktik nəticə: fasiləsiz microtask yaratsan, event növbəsi heç vaxt növbəyə çatmır — proqram "donur".
Isolate ilə async fərqi?`async` **paralellik deyil**: hər şey bir thread-də, bir event loop-da işləyir — gözləmə vaxtından səmərəli istifadə edir, amma CPU işini bölüşdürmür. Isolate isə ayrıca thread və ayrıca yaddaşdır; paylaşılan vəziyyət yoxdur, mesajlaşma var. Ağır JSON parse və ya şəkil emalı üçün `Isolate.run(() => ...)` — 5 saniyəlik hesablama əsas isolate-i bloklamır.
`async` sözü funksiyaya nə edir?Nəticəni avtomatik `Future`-a bükür və funksiyanın içində `await` işlətməyə icazə verir. Vacib detal: gövdə **ilk `await`-ə qədər sinxron** icra olunur — yəni `async` funksiyanı çağırmaq özü-özlüyündə heç nəyi növbəyə salmır.
`sync*` ilə `async*` fərqi?`sync*` lazy `Iterable` qaytarır — dəyərlər tələb olunduqca hesablanır. `async*` isə `Stream` qaytarır — dəyərləri asinxron verir. Hər ikisində `yield` bir element, `yield*` isə bütöv ardıcıllığı ötürür.
Single-subscription ilə broadcast stream fərqi?Single-subscription yalnız bir dəfə dinlənilə bilər — ikinci `listen` xəta atır; hadisələr listener gələnə qədər buferlənir. Broadcast isə çox listener qəbul edir, amma **buferləmir**: gec qoşulan əvvəlki hadisələri görmür. Fayl oxumaq — single; UI hadisələri — broadcast.
`??`, `??=` və `?.` nə edir?`a ?? b` — `a` null olarsa `b`. `a ??= b` — `a` null olarsa ona `b` mənimsədilir. `a?.m()` — `a` null deyilsə çağırır, əks halda `null` qaytarır. Zəncir də mümkündür: `user?.address?.city ?? 'Bakı'`.
`factory` konstruktor nə üçündür?Adi konstruktordan fərqi: **hər dəfə yeni obyekt yaratmağa məcbur deyil**. Keşdən hazır instansiya, alt tip, və ya validasiyadan sonra fərqli obyekt qaytara bilər. `this`-ə çıxışı yoxdur. Tipik istifadə: `factory User.fromJson(...)` və singleton-vari keş.
`const` konstruktor nə verir?Obyekti compile-time-da yaratmaq imkanı: bütün sahələr `final` olmalıdır. Eyni arqumentlərlə yaradılan `const` obyektlər canonicalize olunur — yaddaşda bir nüsxə olur və `identical` `true` qaytarır. Flutter tərəfində bu, yenidən qurulmanın qarşısını aldığı üçün ayrıca dəyərlidir.
Immutable model və `copyWith` niyə standartdır?Dəyişməz obyekt paylaşıla bilər, gözlənilməz mutasiya vermir, `==`/`hashCode` etibarlıdır və keşlənə bilir. Dəyişiklik lazım olanda yeni nüsxə yaradılır: `user.copyWith(email: newEmail)`. Bu, vəziyyət idarəçiliyində "kim bu obyekti dəyişdi?" sualını tamamilə aradan qaldırır.
`identical(a, b)` ilə `a == b` fərqi?`==` məzmun bərabərliyidir (sinif özü təyin edir), `identical` isə eyni obyekt olub-olmadığını yoxlayır. Nümunə: `StringBuffer('dart').toString() == 'dart'` → `true`, amma `identical(...)` → `false`. Gündəlik kodda həmişə `==`; `identical` yalnız canonicalization və keş məsələlərində.

Live coding: proses cavabın özündən vacibdir. Tapşırıq adətən sadədir (bir funksiya, kiçik bir sinif) — qiymətləndirilən şey necə işlədiyindir. Altı addım:

1. Dəqiqləşdir (1-2 dəqiqə). Kodlamağa dərhal başlama. "Giriş nə qədər böyük ola bilər?", "Boş siyahı gələ bilər?", "Sıra vacibdir?", "Nəticə List olsun, yoxsa Iterable kifayətdir?" Bu suallar tapşırığın yarısını həll edir və intervyuerə real iş prosesini göstərir.

2. Nümunə və edge case-lər. Bir sadə nümunə üzərində gözlənilən nəticəni razılaşdır. Sonra sərhədləri say: boş giriş, bir element, təkrarlanan dəyərlər, null, çox böyük giriş. Edge case-ləri əvvəldə demək güclü siqnaldır — sonradan xatırlamaqdan qat-qat yaxşıdır.

3. Yanaşmanı və mürəkkəbliyi səsləndir. "Map ilə qruplaşdırıb sonra çeşidləyəcəm: O(n log n) vaxt, O(n) yaddaş. Alternativ olaraq... amma bu halda daha sadə variant kifayətdir." Mürəkkəbliyi soruşulmadan deməyi vərdiş et.

4. Yazarkən danış. Susqun kodlaşdırma ən çox itirilən baldır. "İndi qruplaşdırma hissəsini yazıram, putIfAbsent istifadə edirəm ki, açar yoxdursa yaransın..."

5. Dry-run. Kodu bitirəndən sonra özün nümunə giriş üzərində sətir-sətir keç: "[3, 1, 3] gəlir, birinci addımda map {3: 2, 1: 1} olur..." Səhvi intervyuerdən əvvəl tapmaq güclü təsir bağışlayır.

6. Trade-off-ları müzakirə et. "Bu variant oxunaqlıdır, amma böyük datada iki keçid edir; performans kritikdirsə bir keçidə yığmaq olar. Test yazsaydım, boş siyahı və təkrarlanan açarlarla başlayardım."

İlişəndə nə etməli — bu, ən çox bal itirilən andır, halbuki düzgün davranış onu demək olar ki, zərərsiz edir:

  • Susma. Səssiz 60 saniyə intervyuer üçün "bilmir" deməkdir. "Bir az fikirləşim" de — bu, tamamilə normaldır.
  • Düşüncəni səsləndir: "İki yanaşma görürəm: sadə O(n²) və map ilə O(n). Əvvəlcə işlək variantı yazım, sonra optimallaşdıraq?" — çoxu intervyuer məhz bunu eşitmək istəyir.
  • İşlək, sadə həll yaz. Brute force + "bunu belə yaxşılaşdıra bilərəm" izahı, yarımçıq "ideal" həlldən yaxşıdır.
  • İpucu istə: "Burada ilişdim — istiqamət verə bilərsiniz?" Bu, mənfi bal deyil; real işdə də kömək istəmək bacarığı dəyərlidir.
  • API-ni xatırlamırsansa: "Metodun dəqiq adını xatırlamıram, groupBy kimi bir şeydir; məntiqi belə yazım, IDE-də dəqiqləşdirərəm." Sintaksis üçün heç kim bal kəsmir; məntiq üçün kəsir.