درس ۱۴ از ۴۹ · داده‌ها و گونه‌ها

ساختارها

ساختار چند داده‌ی مرتبط را زیر یک نام جمع می‌کند و تابع‌هایی به نام روش دارد که روی همان داده کار می‌کنند. برنامه‌های سلام چیزهایی را که با آن‌ها سروکار دارند با ساختار توصیف می‌کنند: یک حساب بانکی، یک سفارش، یک نقطه روی نقشه.

تعریف ساختار

هر فیلد نام و گونه دارد و می‌تواند مقدار پیش‌فرض هم داشته باشد. روش، تابعی است که درون ساختار نوشته شده و درون آن این به مقداری اشاره می‌کند که روش رویش صدا زده شده. فیلدها و روش‌ها به‌طور پیش‌فرض خصوصی‌اند و همگانی آن‌ها را بیرون از ساختار در دسترس می‌گذارد.

structs.salam
ساختار مستطیل:
    همگانی پهنا: صحیح = ۱
    همگانی بلندی: صحیح = ۱
    همگانی تابع مساحت(): صحیح:
        برگشت این.پهنا * این.بلندی
    پایان
پایان

تابع ریشه:
    زمین := مستطیل { پهنا = ۱۲, بلندی = ۸ }
    سرچاپ "مساحت زمین:", زمین.مساحت()
پایان
خروجیمساحت زمین: 96

ساختن و چاپ

در ساختن یک مقدار، نام فیلدهایی را که می‌خواهید تعیین کنید می‌نویسید: نقطه { ایکس = ۳، وای = ۴ }. هر فیلدی که ننویسید مقدار پیش‌فرضش را می‌گیرد. فیلدی که پیش‌فرض ندارد باید هر بار داده شود، وگرنه کامپایلر با پیام E028: مقدار ساختاری 'ن' به فیلد 'ایکس' نیاز دارد می‌ایستد. ساختار را می‌شود مستقیم و با همه‌ی فیلدهایش چاپ کرد:

struct-print.salam
ساختار نقطه:
    همگانی ایکس: صحیح = ۰
    همگانی وای: صحیح = ۰
پایان

تابع ریشه:
    ن := نقطه { ایکس = ۳، وای = ۴ }
    سرچاپ ن
    سرچاپ نقطه {}
پایان
خروجینقطه {ایکس = 3, وای = 4} نقطه {ایکس = 0, وای = 0}

فیلد اختیاری با T؟

فیلدی که مقدار پیش‌فرض دارد همیشه مقدار دارد، پس کدی که آن را می‌خواند نمی‌تواند «تعیین نشده» را از «صفر تعیین شده» جدا کند. برای این کار به گونه‌ی فیلد ؟ اضافه کنید: مهلت: صحیح؟. چنین فیلدی را می‌شود در مقدار ساختاری ننوشت و در این حالت چیزی در خود ندارد، مگر آن‌که پیش‌فرض خودش را داشته باشد، مانند تلاش‌ها: صحیح؟ = ۳. مقدار ساده خودکار در آن قرار می‌گیرد و پوچ دوباره خالی‌اش می‌کند. آن را با دارد()، ندارد()، یابرگردان(جایگزین) یا بازکن() بخوانید، یا از فیلدهای موجود و مقدار استفاده کنید.

struct-optional.salam
ساختار مشتری:
    همگانی میزبان: رشته
    همگانی درگاه: صحیح = ۴۴۳
    همگانی مهلت: صحیح؟
پایان

تابع ریشه:
    الف := مشتری { میزبان = "a.test" }
    سرچاپ الف.درگاه، الف.مهلت.ندارد()
    ناپایا ب := مشتری { میزبان = "b.test"، مهلت = ۳۰ }
    سرچاپ ب.مهلت.مقدار، ب.مهلت.یابرگردان(۵)
    ب.مهلت = پوچ
    سرچاپ ب.مهلت.یابرگردان(۵)
پایان
خروجی443 true 30 30 5

صحیح؟ شکل دیگری از هسته.اختیاری<صحیح> است و برای استفاده از آن به هیچ واردسازی نیازی نیست. ورودی و خروجی تابع هم می‌توانند اختیاری باشند. برای متغیر محلی بنویسید x := ۵ برگردان صحیح؟، یا ناپایا x: صحیح؟ و پیش از خواندن به آن مقدار بدهید. پیش از استفاده از مقدار آن را با دارد() بررسی کنید: در فیلد خالی معنایی ندارد و در یک اختیاری رشته‌ای خالی اشاره‌گر پوچی است که هنگام استفاده برنامه را از کار می‌اندازد. یابرگردان این بررسی را خودش انجام می‌دهد.

فیلدهایی که به هم وابسته‌اند

گاهی فیلدی اختیاری است تا وقتی که فیلد دیگری داده شود: نام کاربر فقط وقتی پذیرفته است که رمز هم باشد. این را با یک قاعده درون ساختار بنویسید تا مترجم هر مقدار ساختاری را با آن بسنجد:

struct-rules.salam
ساختار مشتری:
    همگانی میزبان: رشته
    همگانی کاربر: رشته = ""
    همگانی رمز: رشته = ""
    همگانی توکن: رشته = ""
    قاعده کاربر نیازمند رمز
    قاعده فقط یکی(کاربر، توکن)
پایان

تابع ریشه:
    الف := مشتری { میزبان = "db"، کاربر = "علی"، رمز = "راز" }
    سرچاپ الف.میزبان، الف.کاربر
    ب := مشتری { میزبان = "api"، توکن = "abc" }
    سرچاپ ب.میزبان، ب.توکن
پایان
خروجیdb علی api abc

قاعده الف نیازمند ب، ج یعنی مقداری که الف را تعیین کند باید ب و ج را هم تعیین کند. شکل‌های دیگر روی یک گروه کار می‌کنند: باهم(الف، ب) برای همه یا هیچ‌کدام، فقط یکی(الف، ب)، دست کم یکی(الف، ب) و حداکثر یکی(الف، ب). شکستن قاعده ساخت را با خطا۱۵۹ متوقف می‌کند.

قاعده‌ی نیازمند می‌تواند مقدار را هم بسنجد: قاعده با کلید برابر درست نیازمند کلید فقط وقتی اعمال می‌شود که با کلید درست باشد. مقدار سمت راست باید لفظی یا عضو جداشمار ساده باشد. اگر مقدار فیلد ثابت نباشد، مترجم فرض می‌کند شاید برابر باشد و فیلد را می‌خواهد.

در غیر این صورت قاعده به چیزی نگاه می‌کند که در مقدار نوشته شده، نه به خود مقدارها، پس کاربر = "" هم داده‌شده به حساب می‌آید. قاعده فقط همان مقدار ساختاری را می‌سنجد و تغییر بعدی فیلد بررسی نمی‌شود. اگر چیزی باید همیشه برقرار بماند، از جداشمار داده‌دار یا سازنده استفاده کنید.

متن دلخواه با to_str

شکل چاپی پیش‌فرض همه‌ی فیلدها را نشان می‌دهد؛ برای خودتان هنگام نوشتن برنامه مفید است، ولی همیشه چیزی نیست که کاربر باید ببیند. اگر ساختار روشی با امضای همگانی تابع to_str(): رشته داشته باشد، سرچاپ از آن استفاده می‌کند. وقتی ساختار درون ساختاری دیگر، یک وکتور یا یک آرایه باشد هم همین روش به کار می‌رود.

struct-tostr.salam
ساختار پول:
    همگانی ریال: صحیح = ۰
    همگانی تابع to_str(): رشته:
        تومان := این.ریال / ۱۰
        باقی := این.ریال % ۱۰
        اگر باقی == ۰:
            برگشت "" + تومان + " تومان"
        پایان
        برگشت "" + تومان + " تومان و " + باقی + " ریال"
    پایان
پایان

ساختار کالا:
    همگانی نام: رشته = ""
    همگانی قیمت: پول = پول {}
پایان

تابع ریشه:
    خودکار := کالا { نام = "خودکار"، قیمت = پول { ریال = ۲۵۰۰۰ } }
    سرچاپ خودکار.قیمت
    سرچاپ خودکار
    سرچاپ "جمع: " + پول { ریال = ۱۲۰۵۵ }.ToStr()
پایان
خروجی2500 تومان کالا {نام = "خودکار", قیمت = 2500 تومان} جمع: 1205 تومان و 5 ریال

نام این روش حتی در برنامه‌ی فارسی باید دقیقاً to_str باشد. کامپایلر با همین نام دنبالش می‌گردد و روشی به نام «به رشته» را برای چاپ به کار نمی‌برد.

کالا روش to_str ندارد و فیلدبه‌فیلد چاپ می‌شود، ولی فیلد قیمت آن از روش پول استفاده می‌کند. اگر متن را بدون چاپ لازم دارید، مثل خط آخر خودتان to_str() را صدا بزنید.

همگانی و خصوصی

فیلد خصوصی را فقط روش‌های خود ساختار می‌توانند بخوانند و بنویسند. کدی بیرون از ساختار که ح.موجودی را بخواهد، این خطا را می‌گیرد: E017: فیلد 'موجودی' در ساختار 'حساب' خصوصی است (آن را 'همگانی' کنید). ساختار قاعده‌هایش را این‌طور نگه می‌دارد: این‌جا هیچ‌کس نمی‌تواند موجودی را منفی کند، چون تنها راه تغییرش واریز و برداشت است و هر دو اول مبلغ را بررسی می‌کنند.

struct-private.salam
ساختار حساب:
    دارنده: رشته = ""
    موجودی: صحیح = ۰
    همگانی ایستا تابع باز کن(دارنده: رشته): حساب:
        برگشت حساب { دارنده = دارنده }
    پایان
    همگانی تابع نام دارنده(): رشته:
        برگشت این.دارنده
    پایان
    همگانی تابع مانده(): صحیح:
        برگشت این.موجودی
    پایان
    همگانی ناپایا تابع واریز(مبلغ: صحیح): منطقی:
        اگر مبلغ <= ۰:
            برگشت نادرست
        پایان
        این.موجودی += مبلغ
        برگشت درست
    پایان
    همگانی ناپایا تابع برداشت(مبلغ: صحیح): منطقی:
        اگر مبلغ <= ۰ یا مبلغ > این.موجودی:
            برگشت نادرست
        پایان
        این.موجودی -= مبلغ
        برگشت درست
    پایان
پایان

تابع ریشه:
    ناپایا ح := حساب.باز کن("سارا")
    سرچاپ ح.واریز(۵۰۰)، ح.برداشت(۸۰۰)، ح.برداشت(۱۲۰)
    سرچاپ ح.نام دارنده()، ح.مانده()
پایان
خروجیtrue false true سارا 380

عادت خوبی است که فیلدها را خصوصی نگه دارید مگر دلیلی برای همگانی کردنشان باشد، و به دنیای بیرون روش‌هایی بدهید که بگویند چه کاری ممکن است (واریز)، نه این‌که داده چطور نگه داشته شده (موجودی).

روش نمی‌تواند هم‌نام یکی از فیلدهای ساختار خودش باشد. برای همین روشی که موجودی را برمی‌گرداند مانده نام گرفته، نه موجودی.

روش ناپایا

روشی که فیلدهای ساختار را تغییر می‌دهد با ناپایا تابع تعریف می‌شود. وقتی ساختاری حتی یک روش ناپایا داشته باشد، کامپایلر همه‌ی روش‌هایش را می‌سنجد: تابع معمولی‌ای که به فیلدی از این مقدار بدهد خطاست (E105: در متد 'صفر' نمی‌توان 'این' را تغییر داد: آن را 'ناپایا تابع' اعلام کنید) و روش ناپایا را فقط روی متغیری می‌شود صدا زد که با ناپایا تعریف شده (E106). پس با خواندن خط اول هر روش می‌فهمید که مقدار را تغییر می‌دهد یا نه.

struct-mut.salam
ساختار شمارنده:
    مقدار: صحیح = ۰
    همگانی تابع خواندن(): صحیح:
        برگشت این.مقدار
    پایان
    همگانی ناپایا تابع یکی‌بیشتر():
        این.مقدار++
    پایان
پایان

تابع ریشه:
    ناپایا ش := شمارنده {}
    ش.یکی‌بیشتر()
    ش.یکی‌بیشتر()
    سرچاپ ش.خواندن()
پایان
خروجی2

عضو ایستا و سازنده

ایستا تابع و پایا درون ساختار به خودِ گونه تعلق دارند، نه به یک مقدار؛ با نام ساختار صدایشان بزنید. تابع ایستا این ندارد، ولی می‌تواند فیلدهای خصوصی را مقدار بدهد، و همین آن را جای طبیعی سازنده‌ها می‌کند. حساب.باز کن در مثال قبل یکی از آن‌ها بود؛ این هم یکی دیگر که مقداری آماده می‌سازد:

struct-static.salam
ساختار رنگ:
    همگانی سرخ: صحیح = ۰
    همگانی سبز: صحیح = ۰
    همگانی آبی: صحیح = ۰
    همگانی پایا بیشینه := ۲۵۵
    همگانی ایستا تابع خاکستری(مقدار: صحیح): رنگ:
        برگشت رنگ { سرخ = مقدار، سبز = مقدار، آبی = مقدار }
    پایان
پایان

تابع ریشه:
    سفید := رنگ.خاکستری(رنگ.بیشینه)
    سرچاپ سفید
پایان
خروجیرنگ {سرخ = 255, سبز = 255, آبی = 255}

ساختار مقدار است

نسبت دادن یک ساختار آن را رونویسی می‌کند. پس از مقصد := آغاز این دو مقدار جدا هستند و تغییر مقصد به آغاز کاری ندارد. فرستادن ساختار به یک تابع هم آن را رونویسی می‌کند، پس تابعی که باید ساختارِ صدازننده را تغییر دهد آن را با &: می‌گیرد. دو ساختار را بی هیچ کد اضافه‌ای می‌شود با == و != فیلدبه‌فیلد مقایسه کرد.

struct-copy.salam
ساختار نقطه:
    همگانی ایکس: صحیح = ۰
    همگانی وای: صحیح = ۰
پایان

تابع صفر رونوشت(ن: نقطه):
    ن.ایکس = ۰
پایان

تابع صفر کن(ن &: نقطه):
    ن.ایکس = ۰
پایان

تابع ریشه:
    آغاز := نقطه { ایکس = ۳، وای = ۴ }
    ناپایا مقصد := آغاز
    مقصد.وای = ۱۰
    سرچاپ آغاز، مقصد
    سرچاپ آغاز == نقطه { ایکس = ۳، وای = ۴ }، آغاز == مقصد

    صفر رونوشت(مقصد)
    سرچاپ مقصد.ایکس
    صفر کن(مقصد)
    سرچاپ مقصد.ایکس
پایان
خروجینقطه {ایکس = 3, وای = 4} نقطه {ایکس = 3, وای = 10} true false 3 0

صفر رونوشت رونوشت خودش را تغییر داد که هنگام بازگشت دور ریخته شد. صفر کن ارجاع گرفت و مقصدِ صدازننده را تغییر داد. ورودی &: برای ساختارهای بزرگ ارزان‌تر هم هست، چون چیزی رونویسی نمی‌شود. یک استثنا در «ساختار مقدار است» هست: فیلدی از گونه‌ی وکتور یا نگاشت به شکل یک دستگیره رونویسی می‌شود و هر دو نسخه عضوهای یکسانی را می‌بینند (بخش اشتراک و رونوشت را ببینید).

ترکیب با شامل

شامل یک ساختار را درون ساختار دیگر می‌گذارد و فیلدها و روش‌های همگانی‌اش را مستقیم در دسترس می‌کند. این ترکیب است، نه ارث‌بری: سگ از یک جانور به‌علاوه‌ی نژاد ساخته شده.

struct-embed.salam
ساختار جانور:
    همگانی نام: رشته = ""
    همگانی تابع معرفی(): رشته:
        برگشت "من " + این.نام + " هستم"
    پایان
پایان

ساختار سگ:
    همگانی شامل جانور
    همگانی نژاد: رشته = ""
پایان

تابع ریشه:
    س := سگ { نام = "رکس"، نژاد = "شکاری" }
    سرچاپ س.معرفی()
    سرچاپ س.نام، س.نژاد
پایان
خروجیمن رکس هستم رکس شکاری

ساختارِ شامل‌شده خودش هم فیلدی است به نام گونه‌اش. اگر ساختار بیرونی روشی هم‌نام با روش ساختار درونی تعریف کند، روش خودش برنده است و هنوز می‌تواند نسخه‌ی درونی را از راه همان فیلد صدا بزند:

struct-embed-override.salam
ساختار تاریخچه:
    همگانی ساخت: رشته = ""
    همگانی ویرایش: رشته = ""
    همگانی تابع شرح(): رشته:
        برگشت "ساخته در " + این.ساخت + "، ویرایش در " + این.ویرایش
    پایان
پایان

ساختار نوشته:
    همگانی شامل تاریخچه
    همگانی عنوان: رشته = ""
    همگانی تابع شرح(): رشته:
        برگشت این.عنوان + " (" + این.تاریخچه.شرح() + ")"
    پایان
پایان

تابع ریشه:
    ناپایا ن := نوشته { عنوان = "سلام"، ساخت = "۱۴۰۵/۰۷/۰۹" }
    ن.ویرایش = "۱۴۰۵/۰۷/۱۳"
    سرچاپ ن.شرح()
    سرچاپ ن.ساخت، ن.تاریخچه.ویرایش
    سرچاپ ن
پایان
خروجیسلام (ساخته در ۱۴۰۵/۰۷/۰۹، ویرایش در ۱۴۰۵/۰۷/۱۳) ۱۴۰۵/۰۷/۰۹ ۱۴۰۵/۰۷/۱۳ نوشته {تاریخچه = تاریخچه {ساخت = "۱۴۰۵/۰۷/۰۹", ویرایش = "۱۴۰۵/۰۷/۱۳"}, عنوان = "سلام"}

در ساختن مقدار می‌شود فیلدهای درونی را مستقیم داد (ساخت = ...) و در چاپ، ساختار درونی به شکل مقداری تودرتو نشان داده می‌شود. همگانی شامل ساختار درونی را بیرون هم در دسترس می‌گذارد و شامل تنها آن را خصوصی نگه می‌دارد.

قدم‌های بعدی

چند قاعده‌ی کاربردی

  • به فیلدها پیش‌فرض معقول بدهید تا در ساختن مقدار فقط آن‌چه فرق دارد نوشته شود.
  • فیلدها را خصوصی نگه دارید و روش در اختیار بگذارید؛ فیلد همگانی را برای داده‌ی ساده‌ای بگذارید که قاعده‌ای برای نگهداری ندارد.
  • هر روشی را که ساختار را تغییر می‌دهد ناپایا تابع کنید و متغیری را که روی آن صدایش می‌زنید با ناپایا تعریف کنید.
  • سازنده‌ها را در ایستا تابع بگذارید، به‌ویژه وقتی مقدار باید بررسی شود یا فیلد خصوصی دارد.
  • نسبت دادن و ورودی تابع ساختار را رونویسی می‌کنند. وقتی تابع باید مقدار صدازننده را تغییر دهد، از &: استفاده کنید.