بررسی دقیقتر ویژگیها برای Async
در طول این فصل، از ویژگیهای Future
، Pin
، Unpin
، Stream
، و StreamExt
به روشهای مختلفی استفاده کردهایم. تاکنون، از ورود بیش از حد به جزئیات نحوه کارکرد یا چگونگی تطبیق آنها با یکدیگر اجتناب کردهایم، که برای بیشتر کارهای روزمره شما با Rust کافی است. با این حال، گاهی اوقات با موقعیتهایی مواجه میشوید که نیاز دارید کمی بیشتر از این جزئیات را بفهمید. در این بخش، به اندازهای به این موضوع میپردازیم که در این سناریوها کمک کند، در حالی که بررسی عمیقتر را به مستندات دیگر میسپاریم.
ویژگی Future
بیایید با بررسی دقیقتر نحوه عملکرد ویژگی Future
شروع کنیم. در اینجا نحوه تعریف آن در Rust آمده است:
#![allow(unused)] fn main() { use std::pin::Pin; use std::task::{Context, Poll}; pub trait Future { type Output; fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output>; } }
این تعریف Trait شامل چندین نوع جدید و همچنین نحوی است که قبلاً ندیدهایم، بنابراین بیایید قطعه به قطعه آن را بررسی کنیم.
ابتدا، نوع وابسته Output
در ویژگی Future
مشخص میکند که نتیجه future چه خواهد بود. این شبیه به نوع وابسته Item
در ویژگی Iterator
است. دوم، ویژگی Future
همچنین متد poll
را دارد که یک مرجع خاص Pin
برای پارامتر self
و یک مرجع متغیر به نوع Context
میگیرد و یک Poll<Self::Output>
بازمیگرداند. در ادامه درباره Pin
و Context
بیشتر صحبت خواهیم کرد. فعلاً بیایید روی چیزی که متد بازمیگرداند، یعنی نوع Poll
، تمرکز کنیم:
#![allow(unused)] fn main() { enum Poll<T> { Ready(T), Pending, } }
نوع Poll
شبیه به یک Option
است. این نوع دو حالت دارد: یکی Ready(T)
که شامل یک مقدار است و دیگری Pending
که شامل مقدار نیست. با این حال، Poll
معنای کاملاً متفاوتی از Option
دارد! حالت Pending
نشان میدهد که future هنوز کارهایی برای انجام دادن دارد، بنابراین فراخواننده باید بعداً دوباره بررسی کند. حالت Ready
نشان میدهد که future کار خود را به پایان رسانده و مقدار T
در دسترس است.
نکته: برای بیشتر futures، فراخواننده نباید پس از اینکه future مقدار
Ready
بازگرداند، دوبارهpoll
را فراخوانی کند. بسیاری از futures اگر پس از آماده شدن دوبارهpoll
شوند، دچار وحشت (panic) میشوند. futuresی که ایمن برای poll دوباره هستند، بهطور صریح این موضوع را در مستندات خود ذکر خواهند کرد. این شبیه به نحوه رفتارIterator::next
است.
وقتی کدی را میبینید که از await
استفاده میکند، Rust آن را در پشت صحنه به کدی که poll
را فراخوانی میکند کامپایل میکند. اگر به لیست ۱۷-۴ که در آن عنوان صفحه برای یک URL واحد پس از حلشدن چاپ شد، نگاهی بیندازید، Rust آن را به چیزی که (اگرچه دقیقاً نه، اما تقریباً) شبیه به این است کامپایل میکند:
match page_title(url).poll() {
Ready(page_title) => match page_title {
Some(title) => println!("The title for {url} was {title}"),
None => println!("{url} had no title"),
}
Pending => {
// But what goes here?
}
}
وقتی که future هنوز در حالت Pending
است، چه کاری باید انجام دهیم؟ نیاز داریم به نوعی دوباره امتحان کنیم، و این کار را بارها تکرار کنیم، تا زمانی که future در نهایت آماده شود. به عبارت دیگر، نیاز به یک حلقه داریم:
let mut page_title_fut = page_title(url);
loop {
match page_title_fut.poll() {
Ready(value) => match page_title {
Some(title) => println!("The title for {url} was {title}"),
None => println!("{url} had no title"),
}
Pending => {
// continue
}
}
}
اگر Rust دقیقاً این کد را کامپایل میکرد، هر await
مسدودکننده (blocking) میشد—دقیقاً برعکس چیزی که میخواستیم! در عوض، Rust اطمینان حاصل میکند که حلقه بتواند کنترل را به چیزی واگذار کند که بتواند کار روی این future را متوقف کرده، روی futures دیگر کار کند، و سپس دوباره این یکی را بررسی کند. همانطور که دیدیم، این وظیفه یک runtime async است، و این برنامهریزی و هماهنگی یکی از وظایف اصلی آن است.
در ابتدای فصل، درباره انتظار برای rx.recv
صحبت کردیم. فراخوانی recv
یک future بازمیگرداند و منتظر شدن برای future آن را poll میکند. اشاره کردیم که یک runtime future را تا زمانی که آماده شود—چه با Some(message)
یا با None
در صورت بسته شدن کانال—متوقف میکند. با درک عمیقتر از ویژگی Future
و بهطور خاص Future::poll
، میتوانیم ببینیم این چگونه کار میکند. وقتی future مقدار Poll::Pending
بازمیگرداند، runtime میداند که آماده نیست. برعکس، وقتی poll
مقدار Poll::Ready(Some(message))
یا Poll::Ready(None)
بازمیگرداند، runtime میداند که future آماده است و آن را پیش میبرد.
جزئیات دقیق نحوه انجام این کار توسط یک runtime فراتر از محدوده این کتاب است، اما نکته کلیدی این است که مکانیک پایهای futures را ببینیم: یک runtime هر future که مسئول آن است را poll میکند و وقتی هنوز آماده نیست، future را دوباره به حالت خواب میبرد.
ویژگیهای Pin
و Unpin
وقتی مفهوم pinning را در لیست ۱۷-۱۶ معرفی کردیم، با یک پیام خطای بسیار پیچیده مواجه شدیم. در اینجا بخش مرتبط با آن دوباره آمده است:
error[E0277]: `{async block@src/main.rs:10:23: 10:33}` cannot be unpinned
--> src/main.rs:48:33
|
48 | trpl::join_all(futures).await;
| ^^^^^ the trait `Unpin` is not implemented for `{async block@src/main.rs:10:23: 10:33}`, which is required by `Box<{async block@src/main.rs:10:23: 10:33}>: Future`
|
= note: consider using the `pin!` macro
consider using `Box::pin` if you need to access the pinned value outside of the current scope
= note: required for `Box<{async block@src/main.rs:10:23: 10:33}>` to implement `Future`
note: required by a bound in `futures_util::future::join_all::JoinAll`
--> file:///home/.cargo/registry/src/index.crates.io-6f17d22bba15001f/futures-util-0.3.30/src/future/join_all.rs:29:8
|
27 | pub struct JoinAll<F>
| ------- required by a bound in this struct
28 | where
29 | F: Future,
| ^^^^^^ required by this bound in `JoinAll`
این پیام خطا نه تنها به ما میگوید که باید مقادیر را pin کنیم، بلکه دلیل نیاز به pinning را نیز توضیح میدهد. تابع trpl::join_all
یک ساختار به نام JoinAll
بازمیگرداند. این ساختار به نوعی عمومی به نام F
وابسته است که محدود به پیادهسازی ویژگی Future
است. منتظر شدن مستقیم یک future با await
، future را بهطور ضمنی pin میکند. به همین دلیل نیازی نیست که از pin!
در همه جاهایی که میخواهیم برای futures منتظر بمانیم، استفاده کنیم.
با این حال، ما اینجا مستقیماً منتظر یک future نیستیم. در عوض، یک future جدید به نام JoinAll
میسازیم با ارسال مجموعهای از futures به تابع join_all
. امضای join_all
نیاز دارد که نوع آیتمهای مجموعه، ویژگی Future
را پیادهسازی کنند، و Box<T>
فقط در صورتی ویژگی Future
را پیادهسازی میکند که T
که بستهبندی میکند، یک future باشد که ویژگی Unpin
را پیادهسازی کرده است.
این اطلاعات زیادی برای هضم کردن است! برای درک واقعی آن، بیایید کمی بیشتر به نحوه کار واقعی ویژگی Future
، بهویژه در ارتباط با pinning، بپردازیم.
دوباره به تعریف ویژگی Future
نگاه کنید:
#![allow(unused)] fn main() { use std::pin::Pin; use std::task::{Context, Poll}; pub trait Future { type Output; // متد مورد نیاز fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output>; } }
پارامتر cx
و نوع آن، Context
، کلید اصلی برای این است که یک runtime چگونه میداند چه زمانی یک future خاص را بررسی کند، در حالی که همچنان تنبلی (lazy) باقی میماند. باز هم، جزئیات نحوه کار این فرآیند فراتر از محدوده این فصل است، و معمولاً تنها زمانی که بخواهید یک پیادهسازی سفارشی برای Future
بنویسید، نیاز به فکر کردن به این موضوع دارید. در عوض، ما بر روی نوع self
تمرکز میکنیم، زیرا این اولین باری است که یک متد با یک نوع مشخص برای self
روبرو میشویم. یک نوع مشخص برای self
مانند نوعهای مشخص برای سایر پارامترهای تابع عمل میکند، اما با دو تفاوت کلیدی:
-
به Rust میگوید که نوع
self
برای فراخوانی متد باید چه باشد. -
نمیتواند هر نوعی باشد. این نوع به نوعی که متد روی آن پیادهسازی شده است، یا یک مرجع یا اشارهگر هوشمند به آن نوع، یا یک
Pin
که یک مرجع به آن نوع را بستهبندی میکند، محدود است.
در فصل ۱۸ بیشتر درباره این سینتکس صحبت خواهیم کرد. فعلاً کافی است بدانیم که اگر بخواهیم یک future را poll کنیم تا بررسی کنیم که آیا Pending
یا Ready(Output)
است، به یک مرجع متغیر بستهبندیشده در Pin
برای آن نوع نیاز داریم.
Pin
یک بستهبندی برای انواع اشارهگر مانند &
، &mut
، Box
، و Rc
است. (بهطور فنی، Pin
با نوعهایی کار میکند که ویژگیهای Deref
یا DerefMut
را پیادهسازی میکنند، اما این به طور مؤثر معادل کار با اشارهگرها است.) Pin
خودش یک اشارهگر نیست و هیچ رفتاری مانند Rc
و Arc
که شمارش مرجع انجام میدهند ندارد؛ این صرفاً یک ابزار است که کامپایلر میتواند برای اعمال محدودیتها در استفاده از اشارهگرها استفاده کند.
به یاد آوردن این که await
بر اساس فراخوانیهای poll
پیادهسازی شده است، شروع به توضیح پیام خطایی که قبلاً دیدیم میکند، اما آن پیام در مورد Unpin
بود، نه Pin
. پس دقیقاً چگونه Pin
با Unpin
مرتبط است، و چرا Future
نیاز دارد که self
در یک نوع Pin
باشد تا بتواند poll
را فراخوانی کند؟
به یاد بیاورید که در اوایل این فصل، یک سری از نقاط انتظار (await points) در یک future به یک ماشین حالت کامپایل میشوند، و کامپایلر اطمینان حاصل میکند که این ماشین حالت تمام قوانین معمول ایمنی Rust، از جمله قرضگیری و مالکیت، را دنبال میکند. برای اینکه این کار انجام شود، Rust بررسی میکند که چه دادهای بین یک نقطه انتظار و یا نقطه انتظار بعدی یا پایان بلوک async مورد نیاز است. سپس یک حالت متناظر در ماشین حالت کامپایلشده ایجاد میکند. هر حالت دسترسی لازم به دادههایی که در آن بخش از کد منبع استفاده میشوند را دریافت میکند، چه با گرفتن مالکیت آن دادهها یا با دریافت یک مرجع متغیر یا غیرمتغیر به آن.
تا اینجا خوب است: اگر در مورد مالکیت یا مراجع در یک بلوک async خطایی داشته باشیم، borrow checker به ما اطلاع میدهد. اما وقتی بخواهیم futureای که به آن بلوک مربوط میشود را جابهجا کنیم—مثلاً آن را به یک ساختار داده push کنیم تا بهعنوان یک iterator با join_all
استفاده شود یا آن را از یک تابع بازگردانیم—مسائل پیچیدهتر میشوند.
وقتی یک future را جابهجا میکنیم—چه با push کردن آن به یک ساختار داده برای استفاده بهعنوان iterator با join_all
یا با بازگرداندن آن از یک تابع—این در واقع به معنای جابهجا کردن ماشین حالتی است که Rust برای ما ایجاد میکند. و برخلاف بیشتر انواع دیگر در Rust، futureهایی که Rust برای بلوکهای async ایجاد میکند، میتوانند در فیلدهای هر حالت معین، دارای مراجع به خودشان باشند، همانطور که در تصویر سادهشدهای که در شکل ۱۷-۴ نشان داده شده است.
بهطور پیشفرض، هر شیئی که مرجعی به خودش دارد، جابهجا کردن آن ناایمن است، زیرا مراجع همیشه به آدرس حافظه واقعی چیزی که به آن اشاره میکنند اشاره دارند (نگاه کنید به شکل ۱۷-۵). اگر خود ساختار داده را جابهجا کنید، آن مراجع داخلی همچنان به مکان قدیمی اشاره میکنند. با این حال، آن مکان حافظه اکنون نامعتبر است. از یک طرف، مقدار آن هنگام ایجاد تغییرات در ساختار داده بهروزرسانی نمیشود. از طرف دیگر—و مهمتر—کامپیوتر اکنون میتواند آن مکان حافظه را برای مقاصد دیگر بازاستفاده کند! ممکن است بعداً دادههایی کاملاً نامرتبط بخوانید.
از نظر تئوری، کامپایلر Rust میتواند سعی کند هر مرجع به یک شیء را هر زمان که جابهجا میشود، بهروزرسانی کند، اما این کار میتواند سربار عملکرد زیادی ایجاد کند، بهویژه اگر یک شبکه کامل از مراجع نیاز به بهروزرسانی داشته باشد. اگر بتوانیم به جای آن مطمئن شویم که ساختار داده مورد نظر در حافظه جابهجا نمیشود، نیازی به بهروزرسانی مراجع نخواهیم داشت. این دقیقاً همان چیزی است که borrow checker در Rust نیاز دارد: در کد ایمن، از جابهجا کردن هر آیتمی که مرجع فعالی به آن دارد جلوگیری میکند.
Pin
بر اساس این اصل عمل میکند و تضمین دقیقی که نیاز داریم را ارائه میدهد. وقتی یک مقدار را با بستهبندی یک اشارهگر به آن مقدار در Pin
pin میکنیم، دیگر نمیتواند جابهجا شود. بنابراین، اگر Pin<Box<SomeType>>
داشته باشید، در واقع مقدار SomeType
را pin میکنید، نه اشارهگر Box
. شکل ۱۷-۶ این فرآیند را نشان میدهد.
در واقع، اشارهگر (Pointer) Box
هنوز میتواند بهطور آزاد جابهجا شود. به یاد داشته باشید: ما به مطمئن شدن از اینکه دادهای که در نهایت به آن ارجاع داده میشود در جای خود باقی میماند اهمیت میدهیم. اگر یک اشارهگر (Pointer) جابهجا شود اما دادهای که به آن اشاره میکند در همان مکان باقی بماند، همانطور که در شکل 17-7 نشان داده شده است، هیچ مشکلی پیش نمیآید. (چگونگی انجام این کار با یک Pin
که یک Box
را میپیچد فراتر از بحث این بخش خاص است، اما میتواند تمرین خوبی باشد! اگر به مستندات نوعها و همچنین ماژول std::pin
نگاه کنید، ممکن است بتوانید بفهمید چگونه این کار را انجام دهید.) نکته کلیدی این است که نوع خودارجاعی خود نمیتواند جابهجا شود، زیرا همچنان pin شده است.
با این حال، اکثر انواع کاملاً برای جابهجایی ایمن هستند، حتی اگر پشت یک اشارهگر Pin
قرار داشته باشند. فقط زمانی نیاز به فکر کردن به pinning داریم که آیتمها دارای مراجع داخلی باشند. مقادیر اولیه (primitive) مانند اعداد و مقادیر Boolean ایمن هستند، زیرا بهوضوح هیچ مرجع داخلی ندارند، بنابراین بدیهی است که ایمن هستند. بیشتر انواعی که معمولاً با آنها در Rust کار میکنید نیز اینگونهاند. برای مثال، میتوانید یک Vec
را بدون نگرانی جابهجا کنید. با توجه به آنچه تاکنون دیدهایم، اگر یک Pin<Vec<String>>
داشته باشید، باید همه چیز را از طریق APIهای ایمن اما محدودکنندهای که Pin
ارائه میدهد انجام دهید، حتی اگر یک Vec<String>
همیشه برای جابهجایی ایمن باشد اگر هیچ مرجع دیگری به آن وجود نداشته باشد. ما به روشی نیاز داریم که به کامپایلر بگوییم در چنین مواردی جابهجا کردن آیتمها مشکلی ندارد—و اینجا است که Unpin
وارد عمل میشود.
Unpin
یک ویژگی علامتگذار (marker trait) است، مشابه ویژگیهای Send
و Sync
که در فصل ۱۶ دیدیم، و بنابراین هیچ عملکردی از خود ندارد. ویژگیهای علامتگذار فقط برای این وجود دارند که به کامپایلر بگویند استفاده از نوعی که یک ویژگی خاص را پیادهسازی میکند در یک زمینه خاص ایمن است. Unpin
به کامپایلر اطلاع میدهد که یک نوع خاص نیازی به تضمین اینکه مقدار مربوطه بهصورت ایمن جابهجا میشود، ندارد.
مشابه Send
و Sync
، کامپایلر بهطور خودکار Unpin
را برای تمام انواعی که میتواند ثابت کند ایمن هستند، پیادهسازی میکند. یک مورد خاص، دوباره مشابه Send
و Sync
، این است که Unpin
برای یک نوع پیادهسازی نمیشود. نشانهگذاری برای این حالت به شکل impl !Unpin for SomeType
است، که در آن SomeType
نام نوعی است که باید آن تضمینها را برای ایمن بودن، هر زمان که اشارهگری به آن نوع در یک Pin
استفاده میشود، حفظ کند.
به عبارت دیگر، دو نکته در مورد رابطه بین Pin
و Unpin
باید در نظر داشته باشید. اول، Unpin
حالت “معمولی” است و !Unpin
حالت خاص. دوم، اینکه آیا یک نوع ویژگی Unpin
یا !Unpin
را پیادهسازی میکند فقط زمانی اهمیت دارد که در حال استفاده از یک اشارهگر pin شده به آن نوع مانند Pin<&mut SomeType>
باشید.
برای روشنتر کردن این موضوع، به یک String
فکر کنید: این نوع دارای طول و کاراکترهای Unicode است که آن را تشکیل میدهند. ما میتوانیم یک String
را در Pin
بستهبندی کنیم، همانطور که در شکل ۱۷-۸ دیده میشود. با این حال، String
به طور خودکار ویژگی Unpin
را پیادهسازی میکند، همانطور که بیشتر انواع دیگر در Rust این کار را انجام میدهند.
در نتیجه، میتوانیم کارهایی انجام دهیم که اگر String
ویژگی !Unpin
را پیادهسازی میکرد غیرقانونی بود، مانند جایگزین کردن یک رشته با رشتهای دیگر در همان مکان حافظه، همانطور که در شکل ۱۷-۹ نشان داده شده است. این کار قرارداد Pin
را نقض نمیکند، زیرا String
هیچ مرجع داخلی ندارد که جابهجایی آن را ناایمن کند! این دقیقاً دلیلی است که ویژگی Unpin
را به جای !Unpin
پیادهسازی میکند.
اکنون بهاندازه کافی میدانیم تا خطاهایی که برای آن فراخوانی join_all
در فهرست 17-17 گزارش شدند را درک کنیم. ما در ابتدا سعی کردیم آیندههای تولیدشده توسط بلوکهای async را به یک Vec<Box<dyn Future<Output = ()>>>
منتقل کنیم، اما همانطور که دیدیم، این آیندهها ممکن است ارجاعات داخلی داشته باشند، بنابراین ویژگی Unpin
را پیادهسازی نمیکنند. آنها نیاز به pin شدن دارند، و سپس میتوانیم نوع Pin
را به Vec
ارسال کنیم، با اطمینان از اینکه دادههای زیربنایی در آیندهها جابهجا نخواهند شد.
Pin
و Unpin
بیشتر برای ساخت کتابخانههای سطح پایین یا وقتی که خودتان یک runtime میسازید مهم هستند، نه برای کد روزمره راست. وقتی این Traits را در پیامهای خطا مشاهده میکنید، اکنون ایده بهتری از نحوه رفع کد خواهید داشت!
نکته: این ترکیب
Pin
وUnpin
اجازه میدهد که یک کلاس کامل از نوعهای پیچیده در راست ایمن باشند که در غیر این صورت به دلیل خودارجاعی بودن دشوار برای پیادهسازی هستند. نوعهایی که نیاز بهPin
دارند بیشتر در راست async امروزی ظاهر میشوند، اما ممکن است—بسیار بهندرت!—در زمینههای دیگر نیز ببینید.جزئیات نحوه کار
Pin
وUnpin
و قوانینی که باید رعایت کنند، بهطور گسترده در مستندات API برایstd::pin
پوشش داده شدهاند، بنابراین اگر میخواهید آنها را عمیقتر درک کنید، این مکان خوبی برای شروع است.اگر میخواهید بفهمید که “در پشت صحنه” چگونه کار میکنند، کتاب رسمی برنامهنویسی ناهمگام در راست پاسخگوی شماست:
The Stream Trait
اکنون که درک عمیقتری از Traitsهای Future
، Pin
، و Unpin
داریم، میتوانیم توجه خود را به Trait Stream
معطوف کنیم. همانطور که در بخش معرفی streams توضیح داده شد، streams مشابه iteratorهای ناهمگام هستند. برخلاف Iterator
و Future
، در زمان نگارش این متن، تعریف Stream
در کتابخانه استاندارد وجود ندارد، اما یک تعریف بسیار رایج از crate futures
وجود دارد که در سراسر اکوسیستم استفاده میشود.
بیایید تعاریف Traitsهای Iterator
و Future
را مرور کنیم تا بتوانیم تصور کنیم یک Trait Stream
که این دو را ترکیب میکند چگونه ممکن است به نظر برسد. از Iterator
، مفهوم یک توالی را داریم: متد next
آن یک Option<Self::Item>
فراهم میکند. از Future
، مفهوم آماده شدن در طول زمان را داریم: متد poll
آن یک Poll<Self::Output>
فراهم میکند. برای نمایش یک توالی از آیتمهایی که در طول زمان آماده میشوند، یک Trait Stream
تعریف میکنیم که این ویژگیها را ترکیب میکند:
#![allow(unused)] fn main() { use std::pin::Pin; use std::task::{Context, Poll}; trait Stream { type Item; fn poll_next( self: Pin<&mut Self>, cx: &mut Context<'_> ) -> Poll<Option<Self::Item>>; } }
Trait Stream
یک نوع مرتبط به نام Item
برای نوع آیتمهایی که توسط stream تولید میشوند تعریف میکند. این مشابه با Iterator
است: ممکن است تعداد این آیتمها صفر تا بینهایت باشد، برخلاف Future
که همیشه یک Output
واحد دارد (حتی اگر نوع واحد ()
باشد).
Stream
همچنین یک متد برای دریافت این آیتمها تعریف میکند. ما آن را poll_next
مینامیم تا واضح باشد که این متد به همان روشی که Future::poll
بررسی میکند، آیتمها را بررسی میکند و به همان روشی که Iterator::next
یک توالی از آیتمها تولید میکند، آیتمها را تولید میکند. نوع بازگشتی آن Poll
را با Option
ترکیب میکند. نوع خارجی Poll
است، زیرا باید برای آماده بودن بررسی شود، همانطور که یک آینده بررسی میشود. نوع داخلی Option
است، زیرا باید نشان دهد که آیا پیامهای بیشتری وجود دارد یا نه، همانطور که یک iterator انجام میدهد.
چیزی بسیار مشابه با این احتمالاً در نهایت بهعنوان بخشی از کتابخانه استاندارد راست استانداردسازی خواهد شد. در حال حاضر، این Trait بخشی از ابزار اکثر runtimeها است، بنابراین میتوانید روی آن حساب کنید و همه چیزهایی که در ادامه میبینید عموماً قابل اعمال هستند!
با این حال، در مثالی که در بخش مربوط به streams دیدیم، ما از poll_next
یا Stream
استفاده نکردیم، بلکه از next
و StreamExt
استفاده کردیم. البته میتوانیم مستقیماً از API poll_next
استفاده کنیم و ماشینهای حالت Stream
خود را با دست بنویسیم، همانطور که میتوانیم مستقیماً از طریق متد poll
با آیندهها کار کنیم. اما استفاده از await
بسیار دلپذیرتر است، بنابراین Trait StreamExt
متد next
را فراهم میکند تا بتوانیم دقیقاً این کار را انجام دهیم.
#![allow(unused)] fn main() { use std::pin::Pin; use std::task::{Context, Poll}; trait Stream { type Item; fn poll_next( self: Pin<&mut Self>, cx: &mut Context<'_>, ) -> Poll<Option<Self::Item>>; } trait StreamExt: Stream { async fn next(&mut self) -> Option<Self::Item> where Self: Unpin; // other methods... } }
نکته: تعریف واقعی که قبلاً در این فصل استفاده کردیم کمی متفاوت به نظر میرسد، زیرا از نسخههایی از راست پشتیبانی میکند که هنوز از استفاده از توابع async در Traits پشتیبانی نمیکنند. در نتیجه، اینگونه به نظر میرسد:
fn next(&mut self) -> Next<'_, Self> where Self: Unpin;
نوع
Next
یکstruct
است کهFuture
را پیادهسازی میکند و راهی برای نامگذاری طول عمر ارجاع بهself
باNext<'_, Self>
فراهم میکند، بهطوری کهawait
بتواند با این متد کار کند!
Trait StreamExt
همچنین محل تمام متدهای جالبی است که میتوان با streams استفاده کرد. StreamExt
بهطور خودکار برای هر نوعی که Stream
را پیادهسازی کند، پیادهسازی میشود، اما این Traits بهطور جداگانه تعریف شدهاند تا جامعه بتواند بهصورت جداگانه روی Trait بنیادی و APIهای راحتی کار کند.
در نسخه StreamExt
استفادهشده در crate trpl
، این Trait نه تنها متد next
را تعریف میکند، بلکه یک پیادهسازی از next
ارائه میدهد که جزئیات فراخوانی Stream::poll_next
را بهدرستی مدیریت میکند. این بدان معناست که حتی زمانی که نیاز دارید نوع دادههای جریان خود را بنویسید، فقط کافی است Stream
را پیادهسازی کنید، و سپس هرکسی که از نوع داده شما استفاده کند، میتواند بهطور خودکار از StreamExt
و متدهای آن با آن استفاده کند.
این تمام چیزی است که درباره جزئیات سطح پایین این Traits پوشش خواهیم داد. برای جمعبندی، بیایید در نظر بگیریم که چگونه آیندهها (شامل streams)، تسکها، و نخها همگی با هم سازگار هستند!