مقایسه سلام با جاوا و سی‌شارپ: کد بومی در برابر محیط‌های اجرای مدیریت‌شده

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

در این مقاله
  1. محیط‌های اجرای مدیریت‌شده و کد بومی
  2. یک کار در سه زبان
  3. زمان راه‌اندازی، حافظه و استقرار
  4. زباله‌روب در برابر آزادسازی صریح
  5. یک نقطه‌ی پایانی وب
  6. مقایسه‌ی ویژگی‌های زبان
  7. اکوسیستم، ابزارها و تیم‌ها
  8. سلام، جاوا یا سی‌شارپ: کدام را انتخاب کنیم؟
  9. پرسش‌های پرتکرار

مقایسه‌ی سلام با جاوا و C# در اصل مقایسه‌ی دو شیوه‌ی اجرای برنامه است. جاوا و C# زبان‌های مدیریت‌شده (managed) هستند: کامپایلر بایت‌کد تولید می‌کند (بایت‌کد JVM یا IL در .NET)، و یک محیط اجرا آن را بارگذاری می‌کند، کد داغ را با یک کامپایلر درجا (JIT) به کد ماشین تبدیل می‌کند و حافظه را با زباله‌روب (Garbage Collector) آزاد می‌کند. زبان برنامه‌نویسی سلام پیش از اجرا (AOT) به یک فایل اجرایی بومی کامپایل می‌شود، بدون ماشین مجازی، بدون JIT و بدون زباله‌روب. جاوا و .NET حالا هر دو می‌توانند پیش از اجرا هم کامپایل شوند؛ این فاصله را در برخی جاها کم می‌کند و در برخی جاها نه.

این نوشته توضیح می‌دهد این تفاوت برای زمان راه‌اندازی، حافظه، استقرار و کد روزمره چه معنایی دارد، یک کار را در هر سه زبان نشان می‌دهد، و درباره‌ی برتری‌های بزرگ جاوا و C# در اکوسیستم و ابزارها صریح است. برنامه‌های سلام هنگام ساخت این سایت کامپایل و اجرا می‌شوند؛ نمونه‌ی جاوا روی جاوا ۲۱ و نمونه‌های C# روی .NET 8 یا بالاتر اجرا می‌شوند.

محیط‌های اجرای مدیریت‌شده و کد بومی

JVM و محیط اجرای .NET از پرمهندسی‌ترین نرم‌افزارهای موجودند. یک JIT چندمرحله‌ای (tiered) کار را با تفسیر یا کامپایل سریع متدها شروع می‌کند، می‌بیند کدام کد داغ است و آن را بر پایه‌ی آنچه واقعاً در زمان اجرا رخ می‌دهد با بهینه‌سازی‌های تهاجمی دوباره کامپایل می‌کند. زباله‌روب‌های نسلی (G1 و ZGC در JVM، و GC در .NET) حافظه را بدون اینکه برنامه‌نویس به آن فکر کند پس می‌گیرند. بازتاب (reflection)، بارگذاری پویای کلاس و تولید کد در زمان اجرا همه در دسترس‌اند، چون محیط اجرا آن‌جاست.

جاواC#سلام
کامپایل بهبایت‌کد JVM، سپس JITIL در .NET، سپس JITکد ماشین بومی، پیش از اجرا
گزینه‌ی پیش از اجراGraalVM Native Image؛ کار روی حافظه‌ی نهان AOT در JDKهای اخیرNative AOT، ReadyToRunتنها حالت موجود
حافظهزباله‌روبزباله‌روبمقدارهای روی پشته؛ ظرف‌های هیپ که با آزادکن() و دیرکن آزاد می‌شوند
استقراریک JVM به‌همراه فایل‌های JAR، یا یک native imageمحیط اجرای .NET به‌همراه assemblyها، یا یک فایل بومییک فایل اجرایی
گونه‌های عامدر زمان اجرا پاک می‌شوندماندگار (reified)، تخصیص‌یافته برای گونه‌های مقداریدر زمان کامپایل برای هر گونه جداگانه ساخته می‌شوند
خطاهااستثنا، بررسی‌شده و بررسی‌نشدهاستثناخطادار، تلاش، گرفتن خطا، بدون باز کردن پشته
اکوسیستمMaven Central، Spring، Jakarta EENuGet، ASP.NET Core، Entity Frameworkکتابخانه‌ی استاندارد بزرگ، بسته‌های شخص ثالث اندک

یک کار در سه زبان

سفارش‌ها را بر اساس مشتری گروه‌بندی کنید و جمع هر مشتری را، مرتب‌شده بر اساس نام، چاپ کنید. در جاوا ۲۱ یک record داده را نگه می‌دارد و یک stream گروه‌بندی را انجام می‌دهد:

Java
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import java.util.stream.Collectors;

public class Totals {
    record Order(String customer, double amount) {}

    public static void main(String[] args) {
        List<Order> orders = List.of(
            new Order("sara", 12.5),
            new Order("ali", 7.0),
            new Order("sara", 30.0));

        Map<String, Double> totals = orders.stream()
            .collect(Collectors.groupingBy(Order::customer, TreeMap::new,
                Collectors.summingDouble(Order::amount)));

        totals.forEach((name, sum) -> System.out.printf("%s %.2f%n", name, sum));
    }
}

C# با دستورهای سطح بالا و LINQ از این هم کوتاه‌تر است، و گونه‌ی decimal آن انتخاب طبیعی برای پول است:

C#
var orders = new List<Order>
{
    new("sara", 12.5m),
    new("ali", 7.0m),
    new("sara", 30.0m),
};

foreach (var g in orders.GroupBy(o => o.Customer).OrderBy(g => g.Key))
{
    Console.WriteLine($"{g.Key} {g.Sum(o => o.Amount):F2}");
}

record Order(string Customer, decimal Amount);

سلام کتابخانه‌ای مثل stream یا LINQ ندارد، پس گروه‌بندی یک حلقه‌ی صریح روی یک نگاشت است. نگاشت و وکتور نام‌ها روی هیپ‌اند و وقتی ریشه برمی‌گردد با دیرکن آزاد می‌شوند. نام مشتری‌ها را همان نام‌های لاتین نمونه‌های جاوا و C# گذاشته‌ایم تا خروجی و ترتیبش با آن‌ها یکی باشد:

سلام
واردسازی تبدیل
واردسازی مرتب سازی

ساختار سفارش:
    همگانی مشتری: رشته
    همگانی مبلغ: اعشار۶۴
پایان

تابع ریشه:
    سفارش‌ها := [
        سفارش { مشتری = "sara"، مبلغ = ۱۲.۵ }،
        سفارش { مشتری = "ali"، مبلغ = ۷.۰ }،
        سفارش { مشتری = "sara"، مبلغ = ۳۰.۰ }،
    ]
    ناپایا جمع‌ها := نگاشت {} برگردان نگاشت<رشته، اعشار۶۴>
    دیرکن جمع‌ها.آزادکن()
    هر س در سفارش‌ها:
        جمع‌ها.درج(س.مشتری، جمع‌ها.دریافت با پیش فرض(س.مشتری، ۰.۰) + س.مبلغ)
    پایان
    ناپایا نام‌ها := وکتور {} برگردان وکتور<رشته>
    دیرکن نام‌ها.آزادکن()
    هر نام، _ در جمع‌ها:
        نام‌ها.بیفزا(نام)
    پایان
    مرتب سازی.مرتب(نام‌ها)
    هر نام در نام‌ها:
        سرچاپ نام، تبدیل.قالب اعشار با دقت(جمع‌ها.بگیر(نام)، ۲)
    پایان
پایان
خروجیali 7.00 sara 42.50

نسخه‌ی سلام بلندترینِ هر سه است و این خلاصه‌ی منصفانه‌ی معامله است: جاوا و C# کتابخانه‌های غنی مجموعه‌ها و زباله‌روبی می‌دهند که پشت سرتان را جمع می‌کند، و سلام حلقه‌های ساده و مالکیت صریح. برای پول واقعی، بسته‌ی decimal سلام معادل decimal در C# است.

زمان راه‌اندازی، حافظه و استقرار

JIT برای سریع شدن زمان لازم دارد. یک فرایند JVM یا .NET با بارگذاری محیط اجرا و کتابخانه‌های کلاس شروع می‌شود و کد تا وقتی JIT مسیرهای داغ را کامپایل نکرده کندتر اجرا می‌شود. برای سروری که هفته‌ها روشن است این مهم نیست و بهینه‌سازی‌های JIT بر پایه‌ی رفتار واقعی برنامه می‌توانند کارایی پایدار عالی بدهند. اما برای ابزارهای خط فرمان، تابع‌های بی‌سرور (serverless) که برای هر درخواست راه می‌افتند و کانتینرهایی که مدام بالا و پایین می‌شوند، زمان راه‌اندازی و گرم شدن هزینه‌ی واقعی است، و همین‌طور حافظه‌ای که یک هیپ مدیریت‌شده برای خوب کار کردن لازم دارد.

هر دو سکو پاسخ‌هایی دارند. GraalVM Native Image یک برنامه‌ی جاوا را پیش از اجرا به یک فایل اجرایی بومی کامپایل می‌کند که سریع راه می‌افتد، به بهای فرض «دنیای بسته»: بازتاب، proxyهای پویا و منابع باید در زمان ساخت معلوم باشند یا در پیکربندی اعلام شوند. JDKهای اخیر (۲۴ و ۲۵) نخستین بخش‌های حافظه‌ی نهان پیش از اجرای Project Leyden را هم عرضه کرده‌اند که همان JVM معمولی را نگه می‌دارد اما کار بارگذاری کلاس و پروفایل‌گیری را میان اجراها ذخیره می‌کند. در .NET، Native AOT یک فایل اجرایی بومی خودبسنده می‌سازد و ASP.NET Core از .NET 8 برای minimal APIها از آن پشتیبانی می‌کند؛ این کار کدی سازگار با trim می‌خواهد و از تولید کد در زمان اجرا چشم می‌پوشد. هر دو گزینه‌ی پیش از اجرا همچنان زباله‌روب را با خود دارند.

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

نه جاوا و نه C# در سنجش منتشرشده‌ی سلام حضور ندارند، پس برای این مقایسه عدد اندازه‌گیری‌شده‌ای وجود ندارد. این سنجش سلام را با C، C++، Rust و Go روی ۳۹ برنامه‌ی کوتاه مقایسه می‌کند (میانگین سلام ۱۰٫۶ میلی‌ثانیه، C ۱۱٫۷ و Rust ۱۱٫۳ میلی‌ثانیه بود، با این ملاحظه که راه‌اندازی جزو هر زمان است و فاصله‌های کمتر از حدود ۵٪ نوسان به حساب می‌آیند). برنامه‌های کوتاهی از این دست به سود زبان‌های AOT در برابر زبان‌های JIT تمام می‌شوند، پس در هر حال آزمون منصفانه‌ای برای حالت پایدار یک JVM نیستند.

زباله‌روب در برابر آزادسازی صریح

زباله‌روب یک دسته‌ی کامل از باگ‌ها را حذف می‌کند: نمی‌توانید آزاد کردن حافظه را فراموش کنید و نمی‌توانید آن را دو بار آزاد کنید. زباله‌روب‌های مدرنی مانند ZGC و GC سرور در .NET مکث‌ها را حتی روی هیپ‌های بزرگ کوتاه نگه می‌دارند. هزینه‌ها سربار حافظه (زباله‌روب برای کار کارآمد فضای اضافه لازم دارد)، مقداری پیش‌بینی‌ناپذیری و زمان پردازنده‌ای است که صرف جمع‌آوری می‌شود.

سلام حافظه را صریح آزاد می‌کند. عددها، ساختارها و آرایه‌های ثابت روی پشته زندگی می‌کنند؛ وکتور، نگاشت و ظرف‌های دیگر هیپ با آزادکن() رها می‌شوند، که معمولاً به شکل دیرکن ظرف.آزادکن() درست بعد از ساختن نوشته می‌شود. نتیجه مصرف حافظه‌ی پیش‌بینی‌پذیر و نبود مکث است، و از برنامه‌نویس انضباط می‌خواهد. کامپایلر با گزارش برخی الگوهای استفاده پس از آزادسازی و آزادسازی دوباره کمک می‌کند و salam memcheck برنامه را زیر Valgrind یا AddressSanitizer اجرا می‌کند، اما سلام ایمنی حافظه را اثبات نمی‌کند و آزادسازیِ فراموش‌شده یعنی نشت. برای تیمی که به زباله‌روب عادت دارد، این بزرگ‌ترین تغییر است. مدیریت حافظه در سلام و کامپایل بومی بدون ماشین مجازی و زباله‌روب عمیق‌تر به این موضوع می‌پردازند.

یک نقطه‌ی پایانی وب

سرویس‌های وب جایی است که جاوا و C# در نرم‌افزارهای سازمانی فرمانروایی می‌کنند. یک minimal API در ASP.NET Core از نظر کوتاهی به‌سختی رقیب دارد:

C#
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.MapGet("/hello/{name}", (string name) => new { message = $"Hello, {name}" });

app.Run("http://localhost:8080");

Spring Boot در سمت جاوا کمی تشریفات بیشتر دارد اما تزریق وابستگی، دسترسی به داده، امنیت و مشاهده‌پذیری‌ای را می‌آورد که سازمان‌های بزرگ به آن تکیه می‌کنند. همین نقطه‌ی پایانی در سلام از سرور HTTP و مسیریاب کتابخانه‌ی استاندارد استفاده می‌کند، و ساختار پاسخ با رمزگذار JSONی نوشته می‌شود که کامپایلر خودش می‌سازد. برچسب @جیسون نام کلید را انگلیسی نگه می‌دارد، تا پاسخ همان message نمونه‌ی C# باشد:

سلام
واردسازی شبکه.اچ تی تی پی
واردسازی شبکه.مسیریاب
واردسازی کدگذاری.جیسون

ساختار خوشامد:
    @جیسون "message"
    همگانی پیام: رشته
پایان

تابع سلام کن(زمینه: صحیح۶۴):
    نام := اچ تی تی پی.پارامتر(زمینه، "name")
    اچ تی تی پی.جیسون(زمینه، جیسون.سریال سازی(خوشامد { پیام = "سلام، " + نام }))
پایان

تابع ریشه:
    ناپایا م := مسیریاب.مسیریاب جدید()
    مسیریاب.دریافت(م، "/hello/:name"، سلام کن)
    ناپایا س := اچ تی تی پی.سرور جدید(۸۰۸۰)
    اچ تی تی پی.استفاده(س، م)
    اچ تی تی پی.اجرا(س)
پایان

این سرور تا وقتی متوقفش نکنید اجرا می‌شود، پس این‌جا فقط گونه‌هایش بررسی می‌شود و اجرا نمی‌شود. کتابخانه‌ی استاندارد میان‌افزار، نشست، اعتبارسنجی، مستندسازی OpenAPI و درایورهای PostgreSQL، MySQL، SQLite، Redis و ClickHouse هم دارد، اما چیزی نزدیک به گستردگی Spring یا ASP.NET Core ندارد و ORMی مانند Hibernate یا Entity Framework هم در کار نیست. رویکرد سلام به پایگاه داده به جای آن SQL گونه‌دار است؛ نوشته‌ی برنامه‌های پایگاه داده در سلام را ببینید.

مقایسه‌ی ویژگی‌های زبان

جاوا و C# هر دو به سمت ویژگی‌هایی رفته‌اند که سلام از ابتدا با آن‌ها طراحی شد. جاوا ۲۱ record، واسط‌های مهروموم‌شده (sealed interfaces) و تطبیق الگو در switch دارد که با هم گونه‌های جمع واقعی می‌سازند. C# record، عبارت‌های switch و تطبیق الگو دارد و طراحانش روی گونه‌های union کار کرده‌اند. سلام از روز اول جداشمارهایی دارد که داده حمل می‌کنند، و یک همخوان جامع؛ درس جداشمارها را ببینید.

گونه‌های عام در زیر پوست فرق می‌کنند. جاوا آرگومان‌های گونه را در زمان اجرا پاک می‌کند، پس List<String> و List<Integer> یک کلاس‌اند. C# آن‌ها را نگه می‌دارد و کد عام را برای گونه‌های مقداری تخصیص می‌دهد. سلام هر تابع و ساختار عام را در زمان کامپایل برای هر گونه جداگانه می‌سازد، شبیه قالب‌های C++ اما با قید میانجی به جای duck typing.

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

بازتاب و annotationها بخش بزرگی از دنیای جاوا و .NET را می‌چرخانند: تزریق وابستگی، سریال‌سازها و ORMها گونه‌ها را در حین اجرای برنامه وارسی می‌کنند. سلام بازتاب زمان اجرا ندارد. پشتیبانی JSON و JSON Schema آن را کامپایلر از روی تعریف ساختار تولید می‌کند، و تزریق وابستگی، هر جا که بخواهید، با فراهم‌کننده‌های صریح انجام می‌شود.

هم‌زمانی: نخ‌های مجازی (virtual threads) جاوا ۲۱ و async/await در C# به یک فرایند اجازه می‌دهند با کدی ساده عملیات هم‌زمان بسیار زیادی را مدیریت کند. سلام نخ، یک استخر کار محدود با واژه‌ی کلیدی برو (go)، کانال، future و عملیات اتمی دارد، اما async/await و نخ مجازی ندارد. هم‌زمانی در سلام را ببینید.

اکوسیستم، ابزارها و تیم‌ها

این‌جاست که مقایسه از همه‌جا نابرابرتر است. جاوا و C# چند دهه کتابخانه دارند، چارچوب برای هر الگوی سازمانی، IDEهایی مانند IntelliJ IDEA، Visual Studio و Rider با پشتیبانی عمیق بازآرایی کد، پروفایلرها، عامل‌های پایش، نسخه‌های پشتیبانی بلندمدت از چند فروشنده و میلیون‌ها توسعه‌دهنده. بانک‌ها، دولت‌ها و شرکت‌های بزرگ روی آن‌ها کار می‌کنند و فرایندهای امنیت و انطباقشان پیرامون آن‌ها ساخته شده است.

سلام یک زنجیره‌ی ابزار تک‌فایلی دارد با سرور زبان (LSP)، قالب‌بند، اجراکننده‌ی آزمون، راه‌انداز اشکال‌زدا و مدیر بسته، به‌علاوه‌ی افزونه برای Visual Studio Code و ده ویرایشگر دیگر (زنجیره‌ی ابزار). پیش از نسخه‌ی ۱٫۰ است، جامعه‌اش کوچک است، مخزن بسته‌های شخص ثالثش تازه و تقریباً خالی است و استقرار عملیاتی اندکی دارد. شرکتی که برای یک سامانه‌ی ده‌ساله زبان انتخاب می‌کند باید این را بسنجد. نوشته‌ی چه چیزی یک زبان برنامه‌نویسی را آماده‌ی محیط عملیاتی می‌کند؟ سلام را با همین فهرست می‌سنجد.

سلام، جاوا یا سی‌شارپ: کدام را انتخاب کنیم؟

جاوا یا C# را انتخاب کنید وقتی:

  • سامانه‌های کسب‌وکاری می‌سازید که به Spring، Jakarta EE، ASP.NET Core، یک ORM یا چارچوب‌های بزرگ دیگر وابسته‌اند؛
  • پشتیبانی بلندمدت، پشتوانه‌ی یک فروشنده و بازار کار بزرگ لازم دارید؛
  • سرویس مدت طولانی روشن می‌ماند، پس گرم شدن JIT مهم نیست و کارایی اوج مهم است؛
  • تیمتان زباله‌روب را به کنترل بر حافظه ترجیح می‌دهد.

سلام را انتخاب کنید وقتی:

  • یک فایل اجرایی بومی با راه‌اندازی سریع، حافظه‌ی پیش‌بینی‌پذیر و بدون محیط اجرایی برای نصب می‌خواهید؛
  • ابزار خط فرمان، سرویس کوچک، تابع لبه (edge function) یا جزئی از سیستم می‌نویسید که یک محیط اجرای مدیریت‌شده برایش سربار است؛
  • با آزاد کردن صریح حافظه‌ی هیپ راحتید و گونه‌های جمع و بررسی‌های سخت‌گیرانه‌ی زمان کامپایل می‌خواهید؛
  • می‌خواهید علاوه بر انگلیسی به فارسی هم برنامه بنویسید، برای آموزش یا برای یک تیم محلی.

پرسش‌های پرتکرار

آیا زبان سلام از جاوا یا سی‌شارپ سریع‌تر است؟

هیچ سنجش منتشرشده‌ای آن‌ها را مقایسه نکرده است. سلام از گرم شدن JIT و زباله‌روبی دوری می‌کند، که به زمان راه‌اندازی و حافظه کمک می‌کند، در حالی که JIT در JVM یا .NET می‌تواند روی سرورهای طولانی‌مدت به کارایی پایدار بسیار بالایی برسد. بار کاری خودتان را اندازه بگیرید.

آیا سلام زباله‌روب دارد؟

نه. مقدارهای روی پشته به کاری نیاز ندارند و ظرف‌های هیپ با آزادکن() آزاد می‌شوند، معمولاً از راه دیرکن. کامپایلر و salam memcheck به پیدا کردن اشتباه‌ها کمک می‌کنند.

آیا GraalVM Native Image یا Native AOT در .NET همان کامپایل سلام است؟

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

آیا می‌شود از سلام برای نرم‌افزار سازمانی استفاده کرد؟

می‌تواند سرویس، API و برنامه‌ی پایگاه داده بسازد، اما پیش از نسخه‌ی ۱٫۰ است و چارچوب‌ها، پشتیبانی فروشنده و جامعه‌ی جاوا و .NET را ندارد. برای بیشتر سامانه‌های سازمانی امروز، جاوا یا C# انتخاب کم‌خطرتری است.

سلام و زبان‌های دیگر

مقایسه سلام با پایتون و جولیا برای محاسبات علمی و کار عددی

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

خواندن مقاله
سلام و زبان‌های دیگر

آیا یک زبان برنامه‌نویسی تازه می‌تواند با زبان‌های چنددهه‌ای رقابت کند؟ جایگاه سلام در اکوسیستم جهانی

هر زبان محبوبی روزی تازه بوده و بیشتر زبان‌های تازه هرگز محبوب نمی‌شوند. این نوشته بررسی می‌کند چه چیزی Go، Rust، Kotlin و دیگران را به پذیرش رساند، و سلام را با همان نیروها می‌سنجد، بی‌آنکه وانمود کند فاصله کم است.

خواندن مقاله