Sparround

async*, yield və transformasiyalar

Generator funksiyalar ardıcıllığı əl ilə yığmadan, tələb olunduqca yaradır. Dart-da iki növü var:

  • sync*Iterable<T> qaytarır. Dəyərlər sinxrontələb olunduqca (lazy) hesablanır: iterator.moveNext() çağırılmayınca gövdə işləmir.
  • async*Stream<T> qaytarır. Hər yield bir asinxron data hadisəsidir; gövdədə await etmək olar.

Hər ikisində:

  • yield x — bir dəyər göndərir və funksiyanı həmin nöqtədə dayandırır.
  • yield* other — başqa generator-a deleqasiya edir: onun bütün dəyərləri bu ardıcıllığa "düzlənir". for (final x in other) yield x; yazmaqla eynidir, sadəcə daha səmərəli və oxunaqlıdır.

async*-in mühüm xüsusiyyəti: dinləyici olmayanda işləmir. Subscription pause edilsə generator gövdəsi növbəti yield-də dayanır, cancel edilsə tamamilə dayanır. Bu, StreamController-dən əsas fərqdir — orada belə avtomatik backpressure yoxdur.

OperatorNə edirDiqqət
`map(f)`Hər hadisəni sinxron çevirir`f` async-dırsa `Future` axını alırsan — `asyncMap` lazımdır
`where(test)`Şərti ödəməyən hadisələri atırHadisə sayı azalır, `done` yenə gəlir
`take(n)` / `skip(n)`İlk `n` hadisəni götürür / atır`take` sayı dolanda **upstream-i ləğv edir**
`distinct()`**Ardıcıl** təkrarları atırBütün tarixçəni yadda saxlamır — `a, b, a` üçün üçü də keçir
`asyncMap(f)`Hər hadisə üçün `f`-i gözləyir, **sıranı saxlayır**Paralel işləmir — yavaş `f` bütün axını ləngidir
`expand(f)`Bir hadisəni bir neçəsinə açırSinxrondur; async lazımdırsa `async*` yaz
`transform(t)`Tam nəzarətli `StreamTransformer` tətbiq edirDebounce, throttle, buffer kimi zaman məntiqləri üçün

Debounce və throttle — intuisiya. İkisi də "çox tez-tez gələn hadisələri azaltmaq" üçündür, amma məntiqləri fərqlidir:

  • Debounce — axın susana qədər gözləyir, sonra sonuncu dəyəri buraxır. Axtarış sahəsi üçün budur: istifadəçi yazmağı dayandırandan 300 ms sonra bir sorğu.
  • Throttle — müəyyən intervalda ən çox bir dəyər buraxır. Scroll və ya sensor üçün budur: saniyədə 60 hadisə əvəzinə 10.

Dart-ın öz kitabxanasında hazır debounce yoxdur; ya StreamTransformer.fromHandlers ilə Timer üzərində özün yazırsan, ya da rxdart-ın debounceTime/throttleTime operatorlarını götürürsən.

Backpressure — istehsalçı istehlakçıdan sürətli olanda nə baş verir? Dart-ın cavabı: single-subscription stream-də pause() mənbəni dayandırır (async* generator yield-də gözləyir, fayl oxunuşu dayanır) — bu, real backpressure-dır. Amma StreamController heç nə dayandırmır: add çağırışları buferə yığılır və yaddaş artır. Broadcast stream-də də mənbə dayanmır. Ona görə sürətli mənbə + yavaş istehlakçı ssenarisində ya async* generator (təbii backpressure), ya da açıq şəkildə sample/throttle/buffer məntiqi lazımdır.

*`asyncStreamController` — hansını seçmək?** Praktik qayda:

  • Stream-in dəyərləri kod tərəfindən istehsal olunursa (dövr, səhifələnmiş API-nin bütün səhifələri, retry zənciri) → async*. Daha qısa, ləğvetməni özü idarə edir, backpressure verir və try/finally ilə təmizləmə yazmaq mümkündür.
  • Hadisələr xaricdən gəlirsə (callback API, platform kanalı, manual event bus) → StreamController. Burada sənin "nə vaxt" nəzarətin yoxdur, sadəcə gələni add edirsən.

Bir vacib nüans: async* funksiyasında finally bloku subscription ləğv olunanda da işləyir — resursu bağlamağın etibarlı yolu budur:

  • Stream<String> readLines(File f) async* { final h = await f.open(); try { ... yield line; } finally { await h.close(); } }

StreamController-də isə eyni işi onCancel callback-i ilə əl ilə yazmalısan.

Müsahibə ipucu. İki sual tez-tez gəlir.

"`map` və `asyncMap` fərqi nədir?"map sinxron çevirir; funksiyan async-dırsa nəticə Stream<Future<T>> olur və heç kim onları gözləmir. asyncMap isə hər nəticəni gözləyir. Kritik detalı əlavə et: `asyncMap` paralel işləmir və sıranı saxlayır — yavaş bir çağırış bütün axını ləngidir. Paralellik lazımdırsa hadisələri toplayıb Future.wait etmək və ya rxdart-ın flatMap variantlarını istifadə etmək lazımdır.

"`distinct()` təkrarları atır?" — tələ buradadır: yalnız ardıcıl təkrarları atır. a, b, a ardıcıllığında hər üçü keçir, çünki distinct tarixçə saxlamır. Bu bir cümlə ilə cavab verən namizəd dərhal seçilir.

Ən çox edilən səhv: async*-i StreamController-in sadəcə "qısa yazılışı" saymaq. Fərq backpressure-dadır — async* dinləyici gözləyəndə işini dayandırır, controller isə buferə yığmağa davam edir.

📚 Mənbələr və sənədlər