مطالب
آموزش Linq - بخش ششم : عملگرهای پرس و جو قسمت دوم
در ادامه‌ی سری آموزشی LINQ، عملگر‌های پرس و جوی مرتب سازی، گروه بندی و مجموعه را بررسی خواهیم کرد.

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

عملگر OrderBy

این عملگر توالی ورودی را بر اساس کلیدی که مشخص می‌کنیم مرتب می‌کند.
مثال:
در این مثال کلید معرفی شده‌ی توسط عبارت Lambda، یک رشته است.
string[] ingredients = { "Sugar", "Egg", "Milk", "Flour", "Butter" };
var query = ingredients.OrderBy(x => x);
foreach (var item in query)
{
   Console.WriteLine(item);
}
خروجی مثال بالا:
 Butter
Egg
Flour
Milk
Sugar
همان طور که ملاحظه می‌کنید، عملگر OrderBy به‌صورت پیش فرض مرتب سازی عناصر را صعودی انجام داده است.
عبارت Lambda استفاده شده می‌تواند یک خاصیت از عناصر تشکیل دهنده‌ی توالی ورودی باشد.
مثال:
Ingredient[] ingredients =
{
   new Ingredient {Name = "Sugar", Calories = 500},
   new Ingredient {Name = "Egg", Calories = 100},
   new Ingredient {Name = "Milk", Calories = 150},
   new Ingredient {Name = "Flour", Calories = 50},
   new Ingredient {Name = "Butter", Calories = 200},
};
var query = ingredients.OrderBy(x => x.Calories);
foreach (var item in query)
{
   Console.WriteLine(item.Name + " " + item.Calories);
}
خروجی مثال بالا:
Flour 50
Egg 100
Milk 150
Butter 200
Sugar 500
همانطور که مشاهده می‌کنید توالی خروجی، بر اساس خصوصیت کالری توالی ورودی مرتب شده و در خروجی نمایش داده شده است.

پیاده سازی توسط عبارت‌های جستجو
در عبارت‌های پرس و جو، کلمه کلیدی orderby برای مرتب سازی توالی استفاده می‌شود:
مثال:
var query = from i in ingredients
orderby i.Calories
select i;

عملگر ThenBy

این عملگر می‌تواند به صورت زنجیره‌ای، یک یا چندین بار بعد از عملگر OrderBy در پرس و جو، مورد استفاده قرار گیرد. توسط عملگر ThenBy می‌توان سطوح دیگری از مرتب سازی را اعمال کرد. در مثال زیر ابتدا توالی ورودی را بر اساس کالری و بعد از آن بر اساس نام مرتب خواهیم کرد.
مثال :
Ingredient[] ingredients =
{
   new Ingredient {Name = "Sugar", Calories = 500},
   new Ingredient {Name = "Milk", Calories = 100},
   new Ingredient {Name = "Egg", Calories = 100},
   new Ingredient {Name = "Flour", Calories = 500},
   new Ingredient {Name = "Butter", Calories = 200},
};
var query = ingredients
                     .OrderBy(x => x.Calories)
                     .ThenBy(x => x.Name);
foreach (var item in query)
{
   Console.WriteLine(item.Name + " " + item.Calories);
}
خروجی مثال بالا:
Egg 100
Milk 100
Butter 200
Flour 500
Sugar 500
در اینجا در توالی ورودی، Milk قبل از Egg قرار دارد. ولی به خاطر استفاده از عملگر ThenBy، در مواردی که کالری عناصر یکسان است، بر اساس نام عناصر توالی، مرتب سازی انجام شده است.

پیاده سازی توسط عبارت‌های جستجو
var query = from i in ingredients
orderby i.Calories, i.Name
select i;
همانطور که مشاهده می‌کنید برای مرتب سازی بر اساس خصوصیات دیگر کافیست نام خصوصیت را بعد از اولین عنصر، به وسیله‌ی کاما (,) قید کنید.

عملگر OrderByDescending
این عملگر همچون عملگر OrderBy عمل می‌کند؛ اما توالی ورودی را بر اساس کلید داده شده، به صورت نزولی مرتب می‌کند.
مثال:
Ingredient[] ingredients =
{
   new Ingredient {Name = "Sugar", Calories = 500},
   new Ingredient {Name = "Egg", Calories = 100},
   new Ingredient {Name = "Milk", Calories = 150},
   new Ingredient {Name = "Flour", Calories = 50},
   new Ingredient {Name = "Butter", Calories = 200},
};
var query = ingredients.OrderByDescending(x => x.Calories);
foreach (var item in query)
{
   Console.WriteLine(item.Name + " " + item.Calories);
}
خروجی مثال بالا:
Sugar 500
Flour 500
Butter 200
Milk 100
Egg 100

پیاده سازی توسط عبارت‌های جستجو

جایگزین عملگر OrderByDescending در عبارت‌های جستجو، کلمه‌ی کلیدی descending می‌باشد:
var query = from i in ingredients
orderby i.Calories descending
select i;

عملگر ThenByDescending

این عملگر بصورت زنجیره‌ای بعد از عملگر‌های دیگر می‌آید و مرتب سازی را به‌صورت نزولی انجام می‌دهد.
مثال:
Ingredient[] ingredients =
{
   new Ingredient {Name = "Flour", Calories = 500},
   new Ingredient {Name = "Sugar", Calories = 500},
   new Ingredient {Name = "Egg", Calories = 100},
   new Ingredient {Name = "Milk", Calories = 100},
   new Ingredient {Name = "Butter", Calories = 200}
};
var query = ingredients
                    .OrderBy(x => x.Calories)
                    .ThenByDescending(x => x.Name);
foreach (var item in query)
{
   Console.WriteLine(item.Name + " " + item.Calories);
}
خروجی مثال بالا:
Milk 100
Egg 100
Butter 200
Sugar 500
Flour 500
در این مثال در توالی ورودی، Flour قبل از Sugar آمده است. اما به خاطر عملگر ThenOrderByDescending ترتیب قرار گیری آنها در توالی خروجی تغییر کرده است.

پیاده سازی توسط عبارت‌های جستجو
در عبارت‌های جستجو نیز برای رسیدن به خروجی مشابه کد بالا از کلمه‌ی کلیدی descending استفاده می‌کنیم.
var query = from i in ingredients
orderby i.Calories, i.Name descending
select i;

عملگر Reverse

عملگر Reveres توالی ورودی را دریافت کرده و آن را معکوس و سپس توالی خروجی را تولید می‌کند. اولین عنصر توالی ورودی، آخرین عنصر توالی خروجی می‌باشد.
مثال:
char[] letters = { 'A', 'B', 'C' };
var query = letters.Reverse();
foreach (var item in query)
{
   Console.WriteLine(item);
}
خروجی مثال بالا:
C
B
A

پیاده سازی توسط عبارت‌های جستجو

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


عملگر‌های گروه بندی Grouping Operator

عملگر GroupBy

این عملگر یک توالی ورودی را دریافت کرده و یک توالی گروه بندی شده را به خروجی ارسال می‌کند. پایه‌ای‌ترین امضاء متد GroupBy، یک عبارت Lambda می‌باشد .این عبارت، کلید گروه بندی توالی ورودی را مشخص می‌کند.
مثال: در این مثال قصد داریم مواد غذایی مختلف را بر اساس کالری آنها گروه بندی کنیم.
Ingredient[] ingredients =
{
   new Ingredient {Name = "Sugar", Calories = 500},
   new Ingredient {Name = "Lard", Calories = 500},
   new Ingredient {Name = "Butter", Calories = 500},
   new Ingredient {Name = "Egg", Calories = 100},
   new Ingredient {Name = "Milk", Calories = 100},
   new Ingredient {Name = "Flour", Calories = 50},
   new Ingredient {Name = "Oats", Calories = 50}
};
IEnumerable<IGrouping<int, Ingredient>> query = ingredients.GroupBy(x => x.Calories);
foreach (IGrouping<int, Ingredient> group in query)
{
   Console.WriteLine("Ingredients with {0} calories", group.Key);
   foreach (Ingredient ingredient in group)
   {
     Console.WriteLine(" -{0}", ingredient.Name);
   }
}
در مثال فوق خروجی تابع GroupBy یک لیست قابل شمارش از نوع IGrouping می‌باشد. هر عنصر IGrouping شامل یک کلید (در اینجا کالری مواد غذایی) و لیستی از مواد غذایی که کالری‌های یکسانی دارند، می‌باشد.
خروجی مثال بالا:
 Ingredients with 500 calories
 -Sugar
 -Lard
 -Butter
Ingredients with 100 calories
 -Egg
 -Milk
Ingredients with 50 calories
 -Flour
 -Oats

پیاده سازی توسط عبارت‌های جستجو
در بخش پنجم این سری آموزشی، روش گروه بندی توسط عبارت‌های جستجو توضیح داده شده است.


عملگرهای مجموعه Set Operators
این عملگر‌ها شامل موارد زیر می‌باشند:
• Concat
• Union
• Distinct
• Intersect
• Except

عملگر Concat

این عملگر دو توالی را با هم ادغام می‌کند؛ بطوریکه عناصر توالی دوم، بعد از عناصر توالی اول به توالی خروجی اضافه می‌شوند.
مثال:
string[] applePie = { "Apple", "Sugar", "Pastry", "Cinnamon" };
string[] cherryPie = { "Cherry", "Sugar", "Pastry", "Kirsch" };
IEnumerable<string> query = applePie.Concat(cherryPie);
foreach (string item in query)
{
   Console.WriteLine(item);
}
خروجی مثال بالا :
Apple
Sugar
Pastry
Cinnamon
Cherry
Sugar
Pastry
Kirsch
توجه داشته باشید که در این حالت عناصر تکراری حذف نخواهند شد.

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

عملگر Union
این عملگر همچون عملگر Concat رفتار می‌کند؛ با این تفاوت که عناصر تکراری در توالی خروجی حضور نخواهند داشت.
مثال:
string[] applePie = { "Apple", "Sugar", "Pastry", "Cinnamon" };
string[] cherryPie = { "Cherry", "Sugar", "Pastry", "Kirsch" };
IEnumerable<string> query = applePie.Union(cherryPie);
foreach (string item in query)
{
   Console.WriteLine(item);
}
خروجی مثال بالا:
Apple
Sugar
Pastry
Cinnamon
Cherry
Kirsch
همانطور که مشاهده می‌کنید، عناصر Sugar و Pastry که در توالی دوم تکرار شده بودند، در خروجی وجود ندارند.

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

عملگر  Distinct
این عملگر عناصر تکراری توالی را حذف می‌کند. این عملگر هم می‌تواند بر روی یک توالی اجرا شود و هم می‌تواند بصورت زنجیره‌ای بعد از عملگر‌های دیگر بکار برود.
مثال: استفاده از این عملگر بر روی یک توالی:
string[] applePie = { "Apple", "Sugar", "Apple", "Sugar", "Pastry", "Cinnamon" };
IEnumerable<string> query = applePie.Distinct();
foreach (string item in query)
{
   Console.WriteLine(item);
}
در مثال بالا، عناصر تکراری در توالی ورودی را، از طریق عملگر Distinct حذف کرده‌ایم.
خروجی مثال بالا:
Apple
Sugar
Pastry
Cinnamon

مثال: بکارگیری عملگر Distinct بهمراه عملگر Concat برای شبیه سازی عملیات Union
string[] applePie = { "Apple", "Sugar", "Pastry", "Cinnamon" };
string[] cherryPie = { "Cherry", "Sugar", "Pastry", "Kirsch" };
IEnumerable<string> query = applePie.Concat(cherryPie).Distinct();

foreach (string item in query)
{
   Console.WriteLine(item);
}
خروجی مثال بالا:
Apple
Sugar
Pastry
Cinnamon
Cherry
Kirsch
همانطور که مشاهده می‌کنید خروجی این مثال با حالت استفاده از Union تفاوتی ندارد.

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

عملگر Intersect
این عملگر عناصر مشترک بین دو توالی را باز می‌گرداند.
مثال:
string[] applePie = { "Apple", "Sugar", "Pastry", "Cinnamon" };
string[] cherryPie = { "Cherry", "Sugar", "Pastry", "Kirsch" };
IEnumerable<string> query = applePie.Intersect(cherryPie);
foreach (string item in query)
{
   Console.WriteLine(item);
}
خروجی مثال بالا:
Sugar
Pastry
نکته: عناصر تکراری فقط یکبار در خروجی ظاهر خواهند شد.

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

عملگر Except
این عملگر عناصری از توالی اول را انتخاب می‌کند که در توالی دوم همتایی نداشته باشند.
مثال:
string[] applePie = { "Apple", "Sugar", "Pastry", "Cinnamon" };
string[] cherryPie = { "Cherry", "Sugar", "Pastry", "Kirsch" };
IEnumerable<string> query = applePie.Except(cherryPie);
foreach (string item in query)
{
   Console.WriteLine(item);
}
خروجی مثال فوق:
Apple
Cinnamon
نکته: هیچ عنصری از توالی دوم در خروجی وجود نخواهد داشت.

پیاده سازی توسط عبارت‌های جستجو
معادل این عملگر، کلمه‌ی کلیدی جدیدی در عبارت‌های جستجو وجود ندارد و ترکیب دو روش می‌تواند خروجی دلخواه را تولید کند.
مطالب
شروع به کار با AngularJS 2.0 و TypeScript - قسمت هشتم - دریافت اطلاعات از سرور
اغلب برنامه‌های AngularJS 2.0، اطلاعات خود را از طریق پروتکل HTTP، از سرور دریافت می‌کنند. برنامه یک درخواست Get را صادر کرده و سپس سرور پاسخ مناسبی را ارائه می‌دهد.


مقدمه‌ای بر RxJS

اگر به پیشنیازهای نصب AngularJS 2.0 در قسمت اول این سری دقت کرده باشید، یکی از موارد آن، RxJS است:
"dependencies": {
    "rxjs": "5.0.0-beta.2"
 },
یک Observable، آرایه‌ای است که اعضای آن به صورت غیر همزمان (asynchronously) در طول زمان دریافت می‌شوند. برای مثال پس از شروع یک عملیات async، ابتدا عنصر اول آرایه دریافت می‌شود، پس از مدتی عنصر دوم و در آخر عنصر سوم آن. به همین جهت از Observable‌ها برای مدیریت داده‌های async مانند دریافت اطلاعات از یک وب سرور، استفاده می‌شود.
قرار است Observableها به ES 2016 یا نگارش پس از ES 6 اضافه شوند و یکی از پیشنهادات آن هستند. اما هم اکنون AngularJS 2.0 از این امکان، توسط یک کتابخانه‌ی ثالث، به نام reactive extensions یا Rx، استفاده می‌کند. از RxJS در سرویس HTTP و همچنین مدیریت سیستم رخدادهای AngularJS 2.0 استفاده می‌شود. Observableها امکانی را فراهم می‌کنند تا به ازای دریافت هر اطلاعات async از سرور، بتوان توسط رخداد‌هایی از وقوع آن‌ها مطلع شد.

در نگارش قبلی AngularJS از Promises برای مدیریت اعمال غیرهمزمان استفاده می‌شد. Observableها تفاوت‌های قابل ملاحظه‌ای با Promises دارند:
- یک Promise تنها یک مقدار، یا خطا را بر می‌گرداند؛ اما یک Observable چندین مقدار را در طول یک بازه‌ی زمانی باز می‌گرداند.
- برخلاف Promises، می‌توان عملیات یک Observable را لغو کرد.
- Observableها از عملگرهایی مانند map، reduce، filter و غیره نیز پشتیبانی می‌کنند.

البته باید عنوان کرد که هنوز هم می‌توان از Promises در صورت تمایل در AngularJS 2.0 نیز استفاده کرد.


تنظیمات اولیه‌ی کار با RxJS در AngularJS 2.0

برای استفاده از RxJS در AngularJS 2.0، مراحلی مانند افزودن مدخل اسکریپت http.dev.js، ثبت پروایدر HTTP و importهای لازم، باید طی شوند که در ادامه آن‌ها را بررسی خواهیم کرد:
الف) سرویس HTTP جزئی از angular2/core نیست. به همین جهت مدخل اسکریپت متناظر با آن، باید به صفحه‌ی اصلی سایت اضافه شود که این مورد، در قسمت اول بررسی پیشنیازهای نصب AngularJS 2.0 صورت گرفته‌است:
 <!-- Required for http -->
<script src="~/node_modules/angular2/bundles/http.dev.js"></script>
این تعریف در فایل Views\Shared\_Layout.cshtml (و یا index.html) پروژه‌ی جاری موجود است. همچنین در این صفحه، مدخل Rx.js نیز ذکر شده‌است.

ب) اکنون فایل app.component.ts را گشوده و سرویس HTTP را به آن اضافه می‌کنیم. با نحوه‌ی ثبت سرویس‌ها در قسمت قبل آشنا شدیم:
import { Component } from 'angular2/core';
import { HTTP_PROVIDERS } from 'angular2/http';
import 'rxjs/Rx';   // Load all features
 
import { ProductListComponent } from './products/product-list.component';
import { ProductService } from './products/product.service';
 
@Component({
    selector: 'pm-app',
    template:`
    <div><h1>{{pageTitle}}</h1>
        <pm-products></pm-products>
    </div>
    `,
    directives: [ProductListComponent],
    providers: [
        ProductService,
        HTTP_PROVIDERS
    ]
})
export class AppComponent {
    pageTitle: string = "DNT AngularJS 2.0 APP";
}
از آنجائیکه می‌خواهیم سرویس HTTP، در تمام کامپوننت‌های برنامه در دسترس باشد، آن‌را در بالاترین سطح سلسه مراتب کامپوننت‌های موجود، یا همان کامپوننت ریشه‌ی سایت، ثبت و معرفی می‌کنیم. بنابراین سرویس توکار HTTP یا HTTP_PROVIDERS به لیست پروایدرها، اضافه شده‌است.

ج) پس از آن نیاز است importهای متناظر نیز به ابتدای ماژول فعلی، جهت شناسایی این سرویس و همچنین امکانات rx.js اضافه شوند.
تعریف 'import 'rxjs/Rx به این شکل، به module loader اعلام می‌کند که این کتابخانه را بارگذاری کن، اما چیزی را import نکن. هنگامیکه این کتابخانه بارگذاری می‌شود، کدهای جاوا اسکریپتی آن اجرا شده و سبب می‌شوند که عملگرهای ویژه‌ی Observable آن مانند map و filter نیز در دسترس برنامه قرار گیرند.


ساخت یک سرویس سمت سرور بازگشت لیست محصولات به صورت JSON

چون در ادامه می‌خواهیم لیست محصولات را از سرور دریافت کنیم، برنامه‌ی ASP.NET MVC فعلی را اندکی تغییر می‌دهیم تا این لیست را به صورت JSON بازگشت دهد.
بنابراین ابتدا کلاس مدل محصولات را به نحو ذیل به پوشه‌ی Models اضافه کرده:
namespace MVC5Angular2.Models
{
    public class Product
    {
        public int ProductId { set; get; }
        public string ProductName { set; get; }
        public string ProductCode { set; get; }
        public string ReleaseDate { set; get; }
        public decimal Price { set; get; }
        public string Description { set; get; }
        public double StarRating { set; get; }
        public string ImageUrl { set; get; }
    }
}
و سپس اکشن متد Products، لیست محصولات فرضی این سرویس را بازگشت می‌دهد:
using System.Collections.Generic;
using System.Text;
using System.Web.Mvc;
using MVC5Angular2.Models;
using Newtonsoft.Json;
using Newtonsoft.Json.Serialization;
 
namespace MVC5Angular2.Controllers
{
    public class HomeController : Controller
    {
        // GET: Home
        public ActionResult Index()
        {
            return View();
        }
 
        public ActionResult Products()
        {
            var products = new List<Product>
            {
               new Product
               {
                    ProductId= 2,
                    ProductName= "Garden Cart",
                    ProductCode= "GDN-0023",
                    ReleaseDate= "March 18, 2016",
                    Description= "15 gallon capacity rolling garden cart",
                    Price= (decimal) 32.99,
                    StarRating= 4.2,
                    ImageUrl= "app/assets/images/garden_cart.png"
               },
               new Product
               {
                    ProductId= 5,
                    ProductName= "Hammer",
                    ProductCode= "TBX-0048",
                    ReleaseDate= "May 21, 2016",
                    Description= "Curved claw steel hammer",
                    Price= (decimal) 8.9,
                    StarRating= 4.8,
                    ImageUrl= "app/assets/images/rejon_Hammer.png"
               }
            };
 
            return new ContentResult
            {
                Content = JsonConvert.SerializeObject(products, new JsonSerializerSettings
                {
                    ContractResolver = new CamelCasePropertyNamesContractResolver()
                }),
                ContentType = "application/json",
                ContentEncoding = Encoding.UTF8
            };
        }
    }
}
در اینجا از JSON.NET جهت بازگشت camel case نام خواص مورد نیاز در سمت کاربر، استفاده شده‌است.
برای مطالعه‌ی بیشتر:
«استفاده از JSON.NET در ASP.NET MVC»
«تنظیمات و نکات کاربردی کتابخانه‌ی JSON.NET»

به این ترتیب، آدرس http://localhost:2222/home/products، خروجی JSON سرویس لیست محصولات را در مثال جاری، ارائه می‌دهد.


ارسال یک درخواست HTTP به سرور توسط AngularJS 2.0

اکنون پس از تنظیمات ثبت و معرفی سرویس HTTP و همچنین برپایی یک سرویس سمت سرور ارائه‌ی لیست محصولات، می‌خواهیم سرویس ProductService را که در قسمت قبل ایجاد کردیم (فایل product.service.ts)، جهت دریافت لیست محصولات از سمت سرور، تغییر دهیم:
import { Injectable } from 'angular2/core';
import { IProduct } from './product';
import { Http, Response } from 'angular2/http';
import { Observable } from 'rxjs/Observable';
 
@Injectable()
export class ProductService {
    private _productUrl = '/home/products';
 
    constructor(private _http: Http) { }
 
    getProducts(): Observable<IProduct[]> {
        return this._http.get(this._productUrl)
                         .map((response: Response) => <IProduct[]>response.json())
                         .do(data => console.log("All: " + JSON.stringify(data)))
                         .catch(this.handleError);
    }
 
    private handleError(error: Response) {
        console.error(error);
        return Observable.throw(error.json().error || 'Server error');
    }
}
از آنجائیکه این سرویس دارای یک وابستگی تزریق شده‌است، ذکر ()Injectable@، پیش از تعریف نام کلاس، ضروری است.
در سازنده‌ی کلاس ProductService، کار تزریق وابستگی سرویس Http انجام شده‌است. به این ترتیب با استفاده از متغیر خصوصی http_، می‌توان در کلاس جاری به امکانات این سرویس دسترسی یافت (همان «تزریق سرویس‌ها به کامپوننت‌ها» در قسمت قبل).
سپس متد get آن، یک درخواست HTTP از نوع GET را به آدرس مشخص شده‌ی در متغیر productUrl_ ارسال می‌کند (یا همان سرویس سمت سرور برنامه).
سرویس Http و همچنین شیء Response آن در ماژول‌های Http و Response قرار دارند که در ابتدای صفحه import شده‌اند.

متد http get یک Observable را بازگشت می‌دهد که در نهایت خروجی این متد نیز به همان <[]Observable<IProduct، تنظیم شده‌است. Observable یک شیء جنریک است و در اینجا نوع آن، آرایه‌ای از محصولات درنظر گرفته شده‌است.
اکنون که امضای این متد تغییر یافته است (پیش از این صرفا یک آرایه‌ی ساده از محصولات بود)، استفاده کننده (در کلاس ProductListComponent) باید به تغییرات آن از طریق متد subscribe گوش فرا دهد.
فعلا در کلاس جاری، پس از پایان کار دریافت اطلاعات از سرور، اطلاعات نهایی در متد map در دسترس قرار می‌گیرد (که یکی از عملگرهای RxJs است). کار متد map، اصطلاحا projection است. این متد، هر عضو دریافتی از خروجی سرور را به فرمتی جدید نگاشت می‌کند.
هر درخواست HTTP، در اصل یک عملیات async است. یعنی در اینجا توالی که در اختیار Observable ما قرار می‌گیرد، تنها یک المان دارد که همان شیء HTTP Response است.
بنابراین کار متد map فوق، تبدیل شیء خروجی از سرور، به آرایه‌ای از محصولات است.
در اینجا یک سری کدهای مدیریت استثناءها را نیز در صورت بروز مشکلی می‌توان تعریف کرد. برای مثال در اینجا متد catch، کار پردازش خطاهای رخ داده را انجام می‌دهد.
از متد do جهت لاگ کردن عملیات رخ داده و داده‌های دریافتی در کنسول developer tools مرورگرها استفاده شده‌است.

یک نکته:
اگر خروجی JSON از سرور، برای مثال داخل خاصیتی به نام data محصور شده بود، بجای ()response.json می‌بایستی از response.json().data استفاده می‌شد.


گوش فرا دادن به Observable دریافتی از سرور

تا اینجا یک درخواست HTTP GET را به سمت سرور ارسال کردیم و خروجی آن به صورت Observable در اختیار ما است. اکنون نیاز است کدهای ProductListComponent را جهت گوش فرا دادن به این Observable تغییر دهیم. برای این منظور فایل product-list.component.ts را گشوده و تغییرات ذیل را به آن اعمال کنید:
errorMessage: string;
ngOnInit(): void {
    //console.log('In OnInit');
    this._productService.getProducts()
                        .subscribe(
                              products => this.products = products,
                              error => this.errorMessage = <any>error);
}
در کلاس ProductListComponent، در متد ngOnInit که در آن کار آغاز و مقدار دهی وابستگی‌های کامپوننت انجام می‌شود، متد ()productService.getProducts_ فراخوانی شده‌است. این متد یک Observable را بر می‌گرداند. بنابراین برای پردازش نتیجه‌ی آن نیاز است متد subscribe را در ادامه‌ی آن، زنجیر وار ذکر کرد.
اولین پارامتر متد subscribe، کار دریافت نتایج حاصل را به عهده دارد. برای مثال اگر حاصل عملیات در طی سه مرحله صورت گیرد، سه بار نتیجه‌ی دریافتی را می‌توان در اینجا پردازش کرد. البته همانطور که عنوان شد، یک عملیات غیرهمزمان HTTP، تنها در طی یک مرحله، HTTP Response را دریافت می‌کند؛ بنابراین، پارامتر اول متد subscribe نیز تنها یکبار اجرا می‌شود. در اینجا فرصت خواهیم داشت تا آرایه‌ی دریافتی حاصل از متد map قسمت قبل را به خاصیت عمومی products کلاس جاری نسبت دهیم.
پارامتر دوم متد subscribe در صورت شکست عملیات فراخوانی می‌شود. در اینجا حاصل آن به خاصیت جدید errorMessage نسبت داده شده‌است.


اکنون برنامه را مجددا اجرا کنید، هنوز باید لیست محصولات، مانند قبل نمایش داده شود.


یک نکته
اگر برنامه را اجرا کردید و خروجی مشاهده نشد، به کنسول developer tools مرورگر مراجعه کنید؛ احتمالا خطای ذیل در آن درج شده‌است:
 EXCEPTION: No provider for Http!
به این معنا که پروایدر HTTP یا همان HTTP_PROVIDERS، جایی معرفی نشده‌است. البته مشکلی از این لحاظ در برنامه وجود ندارد و این پروایدر در بالاترین سطح ممکن و در فایل app.component.ts ثبت شده‌است. مشکل اینجا است که مرورگر، فایل قدیمی http://localhost:2222/app/app.component.js را کش کرده‌است (به همراه تمام اسکریپت‌های دیگر) و این فایل قدیمی، فاقد تعریف سرویس HTTP است. بنابراین با حذف کش مرورگر و دریافت فایل‌های js جدید، مشکل برطرف خواهد شد.


کدهای کامل این قسمت را از اینجا می‌توانید دریافت کنید: MVC5Angular2.part8.zip


خلاصه‌ی بحث

برای کار با سرور و ارسال درخواست‌های HTTP، ابتدا نیاز است مدخل تعریف http.dev.js به index.html اضافه شود و سپس HTTP_PROVIDERS را در بالاترین سطح کامپوننت‌های تعریف شده، ثبت و معرفی کرد. پس از آن نیاز است RxJs را نیز import کرد. در ادامه، سرویس دریافت لیست محصولات، وابستگی سرویس HTTP را توسط سازنده‌ی خود دریافت کرده و از آن برای صدور یک فرمان HTTP GET استفاده می‌کند. سپس با استفاده از متد map، کار نگاشت شیء Response دریافتی، به فرمت مناسب مدنظر، صورت می‌گیرد.
در ادامه هر کلاسی که نیاز دارد با این کلاس سرویس دریافت اطلاعات کار کند، متد subscribe را فراخوانی کرده و نتیجه‌ی عملیات را پردازش می‌کند.
مطالب
آموزش LINQ بخش پنجم
در ادامه سری آموزشی LINQ، موارد دیگری از عبارت‌های پرس و جو را بررسی می‌کنیم:
 • عبارت group
 • عبارت orderby
 • کلمات کلیدی ascending و descending
 • کلمه کلیدی by


بررسی عبارت group و کلمه‌ی کلیدی by

عبارت group یک توالی یک بعدی (flat sequence) یا ساده را از ورودی دریافت و یک توالی گروه بندی شده را تولید می‌کند.
مثال: در بخش دوم این سری آموزشی، کلاسی به نام Ingredient تعریف کردیم که مواد غذایی و کالری آنها را نگهداری می‌کرد. در این مثال قصد داریم مواد غذایی دریافت شده از توالی ورودی را بر اساس کالری آنها، به گروه‌هایی تقسیم کنیم، بطوریکه مواد غذایی با کالری‌های یکسان در یک گروه قرار بگیرند:
Ingredient[] ingredients =
{
   new Ingredient{Name = "Sugar", Calories=500},
   new Ingredient{Name = "Lard", Calories=500},
   new Ingredient{Name = "Butter", Calories=500},
   new Ingredient{Name = "Egg", Calories=100},
   new Ingredient{Name = "Milk", Calories=100},
   new Ingredient{Name = "Flour", Calories=50},
   new Ingredient{Name = "Oats", Calories=50}
};

IEnumerable<IGrouping<int, Ingredient>> query =
from i in ingredients
group i by i.Calories;

foreach (IGrouping<int, Ingredient> group in query)
{
   Console.WriteLine($"Ingredients with {group.Key} calories");
   foreach (Ingredient ingredient in group)
   {
      Console.WriteLine($"- { ingredient.Name}");
   }
}
در این مثال، از نوع جنریکی به نام IGrouping استفاده شده‌است که اولین پارامتر آن کلید گروه است (در این مثال کالری مواد غذایی) و دومین پارامتر آن، نوع لیست را مشخص می‌کند که در اینجا کلاس Ingredient است. اعضای لیست تولید شده، کالری یکسانی دارند. دلیل استفاده‌ی از نوع‌ها بصورت صریح و آشکار (Explicit) خوانایی بیشتر برای تشخیص خروجی‌های تولید شده‌ی در کد‌ها می‌باشد. ولی راه بهتر، جایگزین کردن کد <<IEnumerable<IGrouping<int, Ingredient با کلمه‌ی کلیدی var  می‌باشد.
خروجی مثال بالا:
 Ingredients with 500 calories
- Sugar
- Lard
- Butter
Ingredients with 100 calories
- Egg
- Milk
Ingredients with 50 calories
- Flour
- Oats

برای آشنایی بیشتر با اینترفیس IGrouping اینجا را مطالعه کنید.


عبارت orderby و کلمات کلیدی ascending  و descending 

عبارت orderby برای تولید یک توالی مرتب شده‌ی بصورت صعودی (ascending)  و یا نزولی (descending) مورد استفاده قرار می‌گیرد.
مثال: توالی مثال قبل را در نظر بگیرد:
IOrderedEnumerable<Ingredient> sortedByNameQuery =
from i in ingredients
orderby i.Name
select i;

foreach (var ingredient in sortedByNameQuery)
{
   Console.WriteLine(ingredient.Name);
}
خروجی مثال بالا:
Butter
Egg
Flour
Lard
Milk
Oats
Sugar
علملیات مرتب سازی بصورت پیش فرض بصورت صعودی می‌باشد (Ascending). برای تغییر این حالت می‌توانید از کلمه‌ی کلیدی descending استفاده کنید:
IOrderedEnumerable<Ingredient> sortedByNameQuery =
from i in ingredients
orderby i.Name descending
select i;
با اجرای مجدد برنامه، خروجی زیر را مشاهده خواهید کرد:
 Sugar
Oats
Milk
Lard
Flour
Egg
Butter
عبارت orderby  را می‌توان با عبارت groupby ترکیب کرد و نتیجه‌ی حاصل از مثال اول این مطلب را بصورت مرتب شده بر اساس کالری مواد غذایی مشاهده کرد.
مثال:
 Ingredient[] ingredients =
{
   new Ingredient{Name = "Sugar", Calories=500},
   new Ingredient{Name = "Lard", Calories=500},
   new Ingredient{Name = "Butter", Calories=500},
   new Ingredient{Name = "Egg", Calories=100},
   new Ingredient{Name = "Milk", Calories=100},
   new Ingredient{Name = "Flour", Calories=50},
   new Ingredient{Name = "Oats", Calories=50}
};

IEnumerable<IGrouping<int, Ingredient>> query =
from i in ingredients
group i by i.Calories
into calorieGroup
orderby calorieGroup.Key
select calorieGroup;

foreach (IGrouping<int, Ingredient> group in query)
{
   Console.WriteLine($"Ingredients with {group.Key} calories");
   foreach (Ingredient ingredient in group)
   {
     Console.WriteLine($"- { ingredient.Name}");
   }
}
خروجی مثال بالا :
 Ingredients with 50 calories
- Flour
- Oats
Ingredients with 100 calories
- Egg
- Milk
Ingredients with 500 calories
- Sugar
- Lard
- Butter
همانطور که مشاهده می‌کنید بر عکس حالت قبلی که خروجی گروه‌ها بصورت نزولی بود، اینجا خروجی بر اساس کالری غذا و بصورت صعودی، نمایش داده شده است.


در این سری آموزشی با دو روش نوشتن پرس و جو، در LINQ آشنا شدیم:
 1- Fluent Style (استفاده از متدهای الحاقی برای انجام عملیات‌های مختلف بر روی توالی)
 2- Expression Style (استفاده از کلمات کلیدی (key word) برای انجام عملیات‌های مختلف بر روی توالی)

 هر یک از روش‌های فوق مزایایی دارند که با توجه به شرایطی که با آن روبرو هستیم از آنها استفاده می‌کنیم:
 • تعداد عملگر‌ها : در صورتی که پرس و جو نیاز به یک عملگر بر روی توالی داشته باشد می‌توان از روش اول استفاده کرد. به این خاطر که نسبت به روش دوم تعداد دستورات کمتری مورد استفاده قرار خواهد گرفت.
مثال :
 var q1 = ingredients.Where(x => x.Calories > 100);
var q2 = from i in ingredients
 where i.Calories >
100 select i;
در مثال فوق نتیجه‌ی اجرای دو پرس و جو، موادی است که کالری آنها بیشتر از 100 می‌باشد. همانطور که مشاهده می‌کنید روش اول مختصرتر از روش دوم است. این مورد در زمانی است که تنها یک عملیات بر روی توالی اجرا شود.
 • پرس و جویی که قرار است نوشته شود، از نظر عملیات بر روی توالی ساده باشد: در این حالت روش استفاده شده تفاوتی نمی‌کند و بستگی به روش برگزیده و مورد علاقه‌ی برنامه نویس و یا تیم برنامه نویسی دارد. مثلا زمانیکه تنها عملگر‌های Where  و orderby بخواهند اجرا شود.
 • پرس و جوهایی که با متغیر‌های range مختلفی رو برو هستند: در این حالت استفاده از عبارت‌های پرس و جو راحت از روش عملگر‌های پرس و جو می‌باشد.

لیستی از عملگرهای جستجو که در روش عبارت‌های جستجو معادل آنها مهیا شده است :
 • GroupBy
 • GroupJoin
 • Join
 • OrderBy
 • OrderByDescending
 • Select
 • SelectMany
 • ThenBy
 • ThenByDescending
 • Where

در بسیاری از پرس و جو‌ها می‌توانیم هر دو روش را با هم ترکیب کنیم که نمونه‌ای از آن، در جلسه‌ی چهارم در زمان استفاده‌ی از متد DefaultIfEmpty استفاده شد. درمثال زیر استفاده از عملگر count، با استفاده از روش اول، به همراه عبارت پرس و جوی تولید شده‌ی با روش دوم، نمایش داده شده است:
 int mixedQuery =
(from i in ingredients
where i.Calories > 100
select i).Count();
مطالب
الگوی مشاهده‌گر Observer Pattern
الگوی مشاهده‌گر یکی از محبوبترین و معروفترین الگوهای برنامه نویسی است که پیاده سازی آن در بسیاری از زبان‌ها رواج یافته است. برای نمونه پیاده سازی این الگو را می‌توانید در بسیاری از کتابخانه‌ها (به خصوص GUI) مانند این مطالب (+ + + ) مشاهده کنید. برای اینکه بتوانیم این الگو را خودمان برای اشیاء برنامه خودمان پیاده کنیم، بهتر است که بیشتر با خود این الگو آشنا شویم. برای شروع بهتر است که با یک مثال به تعریف این الگو بپردازیم. مثال زیر نقل قولی از یکی از مطالب این سایت است که به خوبی کارکرد این الگو را برای شما نشان میدهد.

یک لامپ و سوئیچ برق را درنظر بگیرید. زمانیکه لامپ مشاهده می‌کند سوئیچ برق در حالت روشن قرار گرفته‌است، روشن خواهد شد و برعکس. در اینجا به سوئیچ، subject و به لامپ، observer گفته می‌شود. هر زمان که حالت سوئیچ تغییر می‌کند، از طریق یک callback، وضعیت خود را به observer اعلام خواهد کرد. علت استفاده از callbackها، ارائه راه‌حل‌های عمومی است تا بتواند با انواع و اقسام اشیاء کار کند. به این ترتیب هر بار که شیء observer از نوع متفاوتی تعریف می‌شود (مثلا بجای لامپ یک خودرو قرار گیرد)، نیازی نخواهد بود تا subject را تغییر داد.

عموما به شیءایی که قرار است وضعیت را مشاهده یا رصد کند، Observer گفته می‌شود و به شیءایی که قرار است وضعیت آن رصد شود Observable یا Subject گفته می‌شود.
 بد نیست بدانید این الگو یکی از کلیدی‌ترین بخش‌های معماری لایه بندی MVC نیز می‌باشد.
 همچنین این نکته حائز اهمیت است که این الگو ممکن است باعث نشتی حافظه هم شود و به این مشکل Lapsed Listener Problem می‌گویند. یعنی یک listener وجود دارد که تاریخ آن منقضی شده، ولی هنوز در حافظه جا خوش کرده‌است. این مشکل برای زبان‌های شیءگرایی که با سیستمی مشابه GC پیاده سازی می‌شوند، رخ میدهد. برای جلوگیری از این حالت، برنامه نویس باید  این مشکل را با رجیستر کردن‌ها و عدم رجیستر یک شنوده، در مواقع لزوم حل کند. در غیر این صورت این شنونده بی جهت، یک ارتباط را زنده نگه داشته و حافظه‌ی منبع را به هدر میدهد.

مثال: ما یک کلید داریم که سه کلاس  RedLED،GreenLED و BlueLED قرار است آن را مشاهده و وضعیت کلید را رصد کنند.
برای پیاده سازی این الگو، ابتدا یک کلاس انتزاعی را با نام Observer که دارای متدی به نام Update است، ایجاد می‌کنیم. متغیر از نوع کلاس Observable را بعدا ایجاد می‌کنیم:
public abstract class Observer
    {
        protected Observable Observable;
        public abstract void Update();
    }
سپس یک کلاس پدر را به نام Observable می‌سازیم تا آن را به شیء سوئیچ نسبت دهیم:
 public  class Observable
    {
        private readonly List<Observer> _observers = new List<Observer>();

        public void Attach(Observer observer)
        {
            _observers.Add(observer);
        }
        public void Dettach(Observer observer)
        {
            _observers.Remove(observer);
        }

        public void NotifyAllObservers()
        {
            foreach (var observer in _observers)
            {
                observer.Update();
            }
        }
    }
در کلاس بالا یک لیست از نوع Observer‌ها داریم که در آن، کلید با تغییر وضعیت خود، لیست رصد کنندگانش را مطلع می‌سازد و دیگر چراغ‌های LED نیازی نیست تا مرتب وضعیت کلید را چک کنند. متدهای attach و Detach در واقع همان رجیستر‌ها هستند که باید مدیریت خوبی روی آن‌ها داشته باشید تا نشتی حافظه پیش نیاید. در نهایت متد NotifyAllObservers هم متدی است که با مرور لیست رصدکنندگانش، رویداد Update آن‌ها را صدا میزند تا تغییر وضعیت کلید به آن‌ها گزارش داده شود.

حال کلاس Switch را با ارث بری از کلاس Observable می‌نویسیم:
 public  class Switch:Observable
    {
        private bool _state;

        public bool ChangeState
        {
            set
            {
                _state = value;
                NotifyAllObservers();
            }
            get { return _state; }
        }
    }
در کلاس بالا هرجایی که وضعیت کلید تغییر می‌یابد، متد NotifyAllObservers صدا زده میشود.
برای هر سه چراغ، رنگی هم داریم:
   public class RedLED:Observer
    {
        private bool _on = false;
        public override void Update()
        {
            _on = !_on;
            Console.WriteLine($"Red LED is {((_on) ? "On" : "Off")}");
        }
    }

  public class GreenLED:Observer
    {
        private bool _on = false;
        public override void Update()
        {
            _on = !_on;
            Console.WriteLine($"Green LED is {((_on) ? "On" : "Off")}");
        }
    }

  public  class BlueLED:Observer
    {
        private bool _on = false;
        public override void Update()
        {
            _on = !_on;
            Console.WriteLine($"Blue LED is {((_on) ? "On" : "Off")}");
        }
    }
سپس در Main اینگونه می‌نویسیم:
var greenLed=new GreenLED();
            var redLed=new RedLED();
            var blueLed=new BlueLED();

            var switchKey=new Switch();
            switchKey.Attach(greenLed);
            switchKey.Attach(redLed);
            switchKey.Attach(blueLed);

            switchKey.ChangeState = true;
            switchKey.ChangeState = false;
به طور خلاصه هر سه چراغ به شیء کلید attach شده و با هر بار عوض شدن وضعیت کلید، متدهای Update هر سه چراغ صدا زده خواهند شد. نتیجه‌ی کد بالا به شکل زیر در کنسول نمایش می‌یابد:
Green LED is On
Red LED is On
Blue LED is On
Green LED is Off
Red LED is Off
Blue LED is Off
مطالب
C# 7 - Tuple return types and deconstruction
روش‌های زیادی برای بازگشت چندین مقدار از یک متد وجود دارند؛ مانند استفاده‌ی از آرایه‌ها برای بازگشت اشیایی از یک جنس، ایجاد یک کلاس سفارشی با خواص متفاوت و استفاده از پارامترهای out و ref همانند روش‌های متداول در C و ++C. در این بین روش دیگری نیز به نام Tuples از زمان NET 4.0. برای بازگشت چندین شیء با نوع‌های مختلف، ارائه شده‌است که در C# 7 نحوه‌ی تعریف و استفاده‌ی از آن‌ها بهبود قابل ملاحظه‌ای یافته‌است.


Tuple چیست؟

هدف از کار با Tupleها، عدم تعریف یک کلاس جدید به همراه خواص آن، جهت بازگشت بیش از یک مقدار از یک متد، توسط وهله‌ای از این کلاس جدید می‌باشد. برای مثال اگر بخواهیم از متدی، دو مقدار شهر و ناحیه را بازگشت دهیم، یک روش آن، ایجاد کلاس مکان زیر است:
public class Location   
{ 
     public string City { get; set; } 
     public string State { get; set; } 
 
     public Location(string city, string state) 
     { 
           City = city; 
           State = state; 
     } 
}
و سپس، وهله سازی و بازگشت آن:
 var location = new Location("Lake Charles","LA");
اما توسط Tuples، بدون نیاز به تعریف یک کلاس جدید، باز هم می‌توان به همین دو خروجی، دسترسی یافت:
 var location = new Tuple<string,string>("Lake Charles","LA");   
// Print out the address
var address = $"{location.Item1}, {location.Item2}";


مشکلات نوع Tuple در نگارش‌های قبلی دات نت

هرچند Tuples از زمان دات نت 4 در دسترس هستند، اما دارای این کمبودها و مشکلات می‌باشند:
static Tuple<int, string, string> GetHumanData()
{
   return Tuple.Create(10, "Marcus", "Miller");
}
الف) پارامترهای خروجی آن‌ها ثابت و با نام‌هایی مانند Item1، Item2 و امثال آن هستند که در حین استفاده، به علت ضعف نامگذاری، کاربرد آن‌ها دقیقا مشخص نیست و کاملا بی‌معنا هستند:
 var data = GetHumanData();
Console.WriteLine("What is this value {0} or this {1}",  data.Item1, data.Item3);
ب) Reference Type هستند (کلاس هستند) و در زمان وهله سازی، میزان مصرف حافظه‌ی بیشتری را نسبت به Value Types (معادل Tuples در C# 7) دارند.
ج) Tuples در دات نت 4، صرفا یک کتابخانه‌ی اضافه شده‌ی به فریم ورک بوده و زبان‌های دات نتی، پشتیبانی توکاری را از آن‌ها جهت بهبود و یا ساده سازی تعریف آن‌ها، ارائه نمی‌دهند.


ایجاد Tuples در C# 7

برای ایجاد Tuples در سی شارپ 7، از پرانتزها به همراه ذکر نام و نوع پارامترها استفاده می‌شود.
(int x1, string s1) = (3, "one");
Console.WriteLine($"{x1} {s1}");
در مثال فوق، یک Tuple ایجاد شده‌است و در آن مقدار 3 به x1 و مقدار "one" به s1 انتساب داده شده‌اند. به این عملیات deconstruction هم می‌گویند.
دسترسی به این مقادیر نیز همانند متغیرهای معمولی است.

اگر سعی کنیم این قطعه کد را کامپایل نمائیم، با خطای ذیل متوقف خواهیم شد:
 error CS8179: Predefined type 'System.ValueTuple`2' is not defined or imported
برای رفع این مشکل نیاز است بسته‌ی نیوگت ذیل را نیز نصب کرد:
 PM> install-package System.ValueTuple

تعاریف متغیرهای بازگشتی، خارج از پرانتزها هم می‌توانند صورت گیرند:
int x2;
string s2;
(x2, s2) = (42, "two");
Console.WriteLine($"{x2} {s2}");


بازگشت Tuples از متدها

متد ذیل، دو خروجی نتیجه و باقیمانده‌ی تقسیم دو عدد صحیح را باز می‌گرداند:
static (int, int) Divide(int x, int y)
{
   int result = x / y;
   int reminder = x % y;
 
   return (result, reminder);
}
برای این منظور، نوع خروجی متد به صورت (int, int) و همچنین مقدار بازگشتی نیز به صورت یک Tuple از نتیجه و باقیمانده‌ی تقسیم، تعریف شده‌است.
در ادامه نحوه‌ی استفاده‌ی از این متد را مشاهده می‌کنید:
 (int result, int reminder) = Divide(11, 3);
Console.WriteLine($"{result} {reminder}");

در اینجا امکان استفاده‌ی از var نیز برای تعریف نوع متغیرهای دریافتی از یک Tuple نیز وجود دارد و کامپایلر به صورت خودکار نوع آن‌ها را بر اساس نوع خروجی tuple مشخص می‌کند:
 (var result1, var reminder1) = Divide(11, 3);
Console.WriteLine($"{result1} {reminder1}");
و یا حتی چون نوع var پارامترها در اینجا یکی است و در هر دو حالت به int اشاره می‌کند، می‌توان این var را در خارج از پرانتز هم قرار داد:
 var (result1, reminder1) = Divide(11, 3);

و یا برای نمونه متد GetHumanData دات نت 4 ابتدای بحث را به صورت ذیل می‌توان در C# 7 بازنویسی کرد:
static (int, string, string) GetHumanData()
{
   return (10, "Marcus", "Miller");
}
و سپس به نحو واضح‌تری از آن استفاده نمود؛ بدون استفاده‌ی اجباری از Item1 و غیره (هرچند هنوز هم می‌توان از آن‌ها استفاده کرد):
 (int Age, string FirstName, string LastName) results = GetHumanData();
Console.WriteLine(results.Age);
Console.WriteLine(results.FirstName);
Console.WriteLine(results.LastName);


پشت صحنه‌ی Tuples در C# 7

همانطور که عنوان شد، برای اینکه بتوانید قطعه کدهای فوق را کامپایل کنید، نیاز به بسته‌ی نیوگت System.ValueTuple است. در حقیقت کامپایلر خروجی متد فوق را به نحو ذیل تفسیر می‌کند:
 ValueTuple<int, int> tuple1 = Divide(11, 3);
برای مثال قطعه کد
 (int, int) n = (1,1);
System.Console.WriteLine(n.Item1);
توسط کامپایلر به قطعه کد ذیل ترجمه می‌شود:
 ValueTuple<int, int> n = new ValueTuple<int, int>(1, 1);
System.Console.WriteLine(n.Item1);
- برخلاف نگارش‌های پیشین دات نت که Tuples در آن‌ها reference type بودند، این ValueTuple یک struct است و به همین جهت سربار تخصیص حافظه‌ی کمتری را به همراه داشته و از لحاظ کارآیی و میزان مصرف حافظه بهینه‌تر عمل می‌کند.
- همچنین در اینجا محدودیتی از لحاظ تعداد پارامترهای ذکر شده‌ی در یک Tuple وجود ندارد.
 (int,int,int,int,int,int,int,(int,int))
در اینجا هم مانند قبل (دات نت 4) 8 آیتم را می‌توان تعریف کرد؛ اما چون آخرین آیتم ValueTuple تعریف شده نیز یک Tuple است، در عمل محدودیتی از نظر تعداد پارامتر نخواهیم داشت.


مفهوم Tuple Literals

همانند نگارش‌های پیشین دات نت، خروجی یک Tuple را می‌توان به یک متغیر از نوع var و یا ValueType نیز نسبت داد:
 var tuple2 = ("Stephanie", 7);
Console.WriteLine($"{tuple2.Item1}, {tuple2.Item2}");
در این حالت برای دسترسی به مقادیر Tuple همانند قبل باید از فیلدهای Item1 و Item2 و ... استفاده کرد.
به علاوه در سی شارپ 7  می‌توان برای اعضای یک Tuple نام نیز تعریف کرد که به آن‌ها Tuple literals گویند:
 var tuple3 = (Name: "Matthias", Age: 6);
Console.WriteLine($"{tuple3.Name} {tuple3.Age}");
در این حالت زمانیکه Tuple به یک متغیر از نوع var نسبت داده می‌شود، می‌توان به خروجی آن بر اساس نام‌های اعضای Tuple، بجای ذکر Item1 و ... دسترسی یافت که خوانایی بیشتری دارند.

و یا هنگام تعریف نوع خروجی، می‌توان نام پارامترهای متناظر را نیز ذکر کرد که به آن named elements هم می‌گویند:
static (int radius, double area) CalculateAreaOfCircle(int radius)
{
   return (radius, Math.PI * Math.Pow(radius, 2));
}
و نمونه‌ای از کاربرد آن به صورت ذیل است که در اینجا خروجی Tuple صرفا به یک متغیر از نوع var نسبت داده شده‌است و توسط نام پارامترهای خروجی متد، می‌توان به اعضای Tuple دسترسی یافت.
 var circle = CalculateAreaOfCircle(2);
Console.WriteLine($"A circle of radius, {circle.radius}," +
 $" has an area of {circle.area:N2}.");


مفهوم Deconstructing Tuples

مفهوم deconstruction که در ابتدای بحث عنوان شد صرفا مختص به Tuples نیست. در C# 7 می‌توان مشخص کرد که چگونه یک نوع خاص، به اجزای آن تجزیه شود. برای مثال کلاس شخص ذیل را درنظر بگیرید:
class Person
{
    private readonly string _firstName;
    private readonly string _lastName;
 
    public Person(string firstname, string lastname)
    {
        _firstName = firstname;
        _lastName = lastname;
    }
 
    public override String ToString() => $"{_firstName} {_lastName}";
 
    public void Deconstruct(out string firstname, out string lastname)
    {
        firstname = _firstName;
        lastname = _lastName;
    }
}
- در اینجا یک متد جدید را به نام Deconstruct مشاهده می‌کنید. کار این متد جدید که توسط کامپایلر استفاده خواهد شد، ارائه‌ی روشی است برای «تجزیه‌ی» یک نوع، به یک Tuple‌. متد Deconstruct تعریف شده‌ی در اینجا توسط پارامترهایی از نوع out، دو خروجی را مشخص می‌کنند. امکان تعریف این متد ویژه، به صورتیکه یک Tuple را بازگرداند، وجود ندارد.
- علت تعریف این دو خروجی هم به constructor و یا سازنده‌ی کلاس بر می‌گردد که دو ورودی را دریافت می‌کند. اگر یک کلاس چندین سازنده داشته باشد، به همان تعداد می‌توان متد Deconstruct تعریف کرد؛ به همراه خروجی‌هایی متناظر با نوع پارامترهای سازنده‌ها.
- علت استفاده‌ی از نوع خروجی out نیز این است که در #C نمی‌توان چندین overload را صرفا بر اساس نوع خروجی‌های متفاوت متدها تعریف کرد.
- متد Deconstruct به صورت خودکار در زمان تجزیه‌ی یک شیء به یک tuple فراخوانی می‌شود. در مثال زیر، شیء p1 به یک Tuple تجزیه شده‌است و این تجزیه بر اساس متد Deconstruct این کلاس مفهوم پیدا می‌کند:
 var p1 = new Person("Katharina", "Nagel");
(string first, string last) = p1;
Console.WriteLine($"{first} {last}");


امکان تعریف متد Deconstruct‌، به صورت یک متد الحاقی

روش اول تعریف متد ویژه‌ی Deconstruct را در مثال قبل، در داخل کلاس اصلی مشاهده کردید. روش دیگر آن، استفاده‌ی از متدهای الحاقی است که در این مورد خاص نیز مجاز است:
public class Rectangle
{
    public Rectangle(int height, int width)
    {
        Height = height;
        Width = width;
    }
 
    public int Width { get; }
    public int Height { get; }
}
 
public static class RectangleExtensions
{
    public static void Deconstruct(this Rectangle rectangle, out int height, out int width)
    {
        height = rectangle.Height;
        width = rectangle.Width;
    }
}
در اینجا کلاس مستطیل دارای سازنده‌ای با دو پارامتر است؛ اما متد Deconstruct آن به صورت یک متد الحاقی، خارج از کلاس اصلی تعریف شده‌است.
اکنون امکان انتساب وهله‌ای از این کلاس به یک Tuple وجود دارد:
 var r1 = new Rectangle(100, 200);
(int height, int width) = r1;
Console.WriteLine($"height: {height}, width: {width}");


امکان جایگزین کردن Anonymous types با Tuples

قطعه کد ذیل را در نظر بگیرید:
List<Employee> allEmployees = new List<Employee>()
{
  new Employee { ID = 1L, Name = "Fred", Salary = 50000M },
  new Employee { ID = 2L, Name = "Sally", Salary = 60000M },
  new Employee { ID = 3L, Name = "George", Salary = 70000M }
};
var wellPaid =
  from oneEmployee in allEmployees
  where oneEmployee.Salary > 50000M
  select new { EmpName = oneEmployee.Name,
               Income = oneEmployee.Salary };
در اینجا خروجی LINQ تهیه شده یک لیست anonymously typed است؛ با محدودیت‌هایی مانند عدم امکان استفاده‌ی از خروجی آن در سایر اسمبلی‌ها. این نوع‌های ویژه تنها محدود هستند به همان اسمبلی که در آن تعریف می‌شوند. اما در C# 7 می‌توان قطعه کد فوق را با Tuples به صورت ذیل بازنویسی کرد که این محدودیت‌ها را هم ندارد (با هدف به حداقل رساندن تعداد ViewModel‌های تعریفی یک برنامه):
var wellPaid =
  from oneEmployee in allEmployees
  where oneEmployee.Salary > 50000M
  orderby oneEmployee.Salary descending
  select (EmpName: oneEmployee.Name,
          Income: oneEmployee.Salary);
var highestPaid = wellPaid.First().EmpName;


سایر کاربردهای Tuples

از Tuples صرفا برای تعریف چندین خروجی از یک متد استفاده نمی‌شود. در ذیل نحوه‌ی استفاده‌ی از آن‌ها را جهت تعریف کلید ترکیبی یک شیء دیکشنری و یا استفاده‌ی از آن‌ها را در آرگومان جنریک یک متد async هم مشاهده می‌کنید:
public Task<(int index, T item)> FindAsync<T>(IEnumerable<T> input, Predicate<T> match)
{
   var dictionary = new Dictionary<(int, int), string>();
   throw new NotSupportedException();
}
مطالب
روش‌هایی برای مدیریت بهتر عملگرهای RxJS در برنامه‌های Angular
کتابخانه‌ی RxJS، جزو پایه‌ای کار با برنامه‌های Angular است و ساده‌ترین روش کار با آن، تعریف یک سطر ذیل است:
 import {Observable} from 'rxjs';
به این ترتیب تمام عملگرهای RxJS مانند map ،do ،catch و غیره نیز import خواهند شد. اما این سادگی ... به قیمت افزوده شدن یک بسته‌ی 586 KB (غیرفشرده) به فایل‌های نهایی، تمام خواهد شد.


روش‌های مختلف import ویژگی‌های کتابخانه‌ی RxJS

الف) import همه چیز به صورت یکجا
 import Rx from "rxjs/Rx";
یک مثال
 Rx.Observable.of(1, 2, 3).map(i => i.toString());
این روش بسیار ساده‌است. اما سبب import کامل یک کتابخانه‌ی 586 KB نیز می‌شود.

ب) تنها import ویژگی‌های مورد نیاز
import { Observable } from "rxjs/Observable";
import "rxjs/add/observable/of";
import "rxjs/add/operator/map";
به این روش «patching the Observable prototype» نیز گفته می‌شود.
یک مثال
 Observable.of(1, 2, 3).map(i => i.toString());
مزیت این روش، کاهش قابل ملاحظه‌ی حجم نهایی برنامه است. فقط باید دقت داشت که متدهای مورد نیاز، حتما import شده باشند.

ج) فراخوانی مستقیم متدهای RxJS نه از طریق Observable
import { Observable } from "rxjs/Observable";
import { of } from "rxjs/observable/of";
import { map } from "rxjs/operator/map";
یک مثال
 const source = of(1, 2, 3);
const mapped = map.call(source, i => i.toString());
در این حالت کد بیشتری باید نوشته شود و مزیت اطلاعات نوع متد را نیز از دست می‌دهیم. از این جهت که این متدها زمانیکه با این روش import می‌شوند، any را بازگشت می‌دهند (Function.prototype.call خروجی از نوع any دارد).


مقید کردن برنامه به عدم استفاده از حالت «الف» و اجبار به استفاده از حالت «ب»

اگر به ریشه‌ی پوشه‌ی پروژه‌های مبتنی بر Angular CLI دقت کنید، فایل tslint.json نیز در آن‌ها قابل مشاهده است و اگر افزونه‌ی VSCode آن‌را نیز نصب کرده باشید، در حین کار با VSCode، خطاهای مرتبط را درون ادیتور مشاهده خواهید کرد. TSLint، قابلیت توسعه داشته و یک نمونه‌ی از این‌ها، بسته‌ی TSLint rules for RxJS است. برای نصب آن ابتدا دستور ذیل را صادر کنید:
 npm install rxjs-tslint-rules --save-dev
سپس فایل tslint.json را گشوده و تغییرات ذیل را به آن اعمال نمائید:
{
  "rulesDirectory": [
    "node_modules/codelyzer"
  ],
  "extends": [
    "rxjs-tslint-rules"
  ],  
  "rules": {
    "rxjs-add": { "severity": "error" },
    "rxjs-no-patched": { "severity": "error" },
    "rxjs-no-unused-add": { "severity": "error" },
    "rxjs-no-wholesale": { "severity": "error" },
    "rxjs-no-subject-unsubscribe": { "severity": "error" },
rxjs-no-wholesale آن سبب منع درج حالت «الف» می‌شود و سایر حالات روش «ب» را تشویق می‌کنند.



مدیریت بهتر حالت «ب» یا «تنها import ویژگی‌های مورد نیاز»

زمانیکه از روش «ب» استفاده می‌کنیم، کلاس‌های سرویس برنامه پر خواهند شد از کدهای تکراری ذیل:
import 'rxjs/add/operator/map';
import 'rxjs/add/operator/do';
import 'rxjs/add/operator/catch';
در اینجا هر متدی را که نیاز است باید یکبار import کرد و این‌کار را باید به ازای تک‌تک سرویس‌های برنامه نیز تکرار نمود.
برای مدیریت بهتر اینکار، فایل جدیدی را به نام src\app\shared\rxjs-operators.ts ایجاد می‌کنیم؛ با محتوای ذیل:
// define the rxjs operators needed by your app
// see node_module/rxjs/Rx.js for more

// statics
import "rxjs/add/observable/from";
import "rxjs/add/observable/throw";

// operators
import "rxjs/add/operator/catch";
import "rxjs/add/operator/combineLatest";
import "rxjs/add/operator/debounceTime";
import "rxjs/add/operator/delay";
import "rxjs/add/operator/distinctUntilChanged";
import "rxjs/add/operator/do";
import "rxjs/add/operator/filter";
import "rxjs/add/operator/finally";
import "rxjs/add/operator/first";
import "rxjs/add/operator/ignoreElements";
import "rxjs/add/operator/let";
import "rxjs/add/operator/map";
import "rxjs/add/operator/mapTo";
import "rxjs/add/operator/mergeMap";
import "rxjs/add/operator/startWith";
import "rxjs/add/operator/switchMap";
import "rxjs/add/operator/takeUntil";
import "rxjs/add/operator/withLatestFrom";
import "rxjs/add/operator/takeUntil";
import "rxjs/add/operator/take";
در اینجا تمام importهای مورد نیاز برنامه به صورت یکجا درج می‌شوند.
سپس کافی است به فایل src\app\app.module.ts مراجعه کرده و این فایل را import کنیم:
 // import RxJs needed operators only once
import "./shared/rxjs-operators";

//...
@NgModule({
//...
با اینکار سبب خواهیم شد تا دیگر در سرویس‌های برنامه (و تمام قسمت‌های آن) نیازی به تعریف‌های تکراری RxJS وجود نداشته باشد و تنها تعریفی که در آنجا نیاز است، یک مورد ذیل است:
 import { Observable } from "rxjs/Observable";
علت اینجا است، همانطور که عنوان شد، به این روش «patching the Observable prototype» نیز گفته می‌شود. به این معنا که این عملگرها و یا افزونه‌ها، جزئی از کلاس Observable می‌شوند و در تمام قسمت‌های برنامه، پس از import آن‌ها قابل دسترسی خواهند بود. بنابراین با یکبار import فایل rxjs-operators.ts، دیگر نیازی به تعریف مجدد آن نخواهد بود.
مطالب
بررسی ساختارهای جدید DateOnly و TimeOnly در دات نت 6
به همراه دات نت 6، دو ساختار داده‌ی جدید DateOnly و TimeOnly نیز معرفی شده‌اند که امکان کار کردن ساده‌تر با قسمت‌های فقط تاریخ و یا فقط زمان DateTime را میسر می‌کنند. این دو نوع جدید نیز همانند DateTime، از نوع struct هستند و بنابراین value type محسوب می‌شوند. در فضای نام System قرار گرفته‌اند و همچنین با نوع‌های date و time مربوط به SQL Server، سازگاری کاملی دارند.


روش استفاده از نوع DateOnly در دات نت 6

نوع‌های جدید معرفی شده، بسیار واضح هستند و مقصود از بکارگیری آن‌ها را به خوبی بیان می‌کنند. برای مثال اگر نیاز بود تاریخی را بدون در نظر گرفتن قسمت زمان آن معرفی کنیم، می‌توان از نوع DateOnly استفاده کرد؛ مانند تاریخ تولد، روزهای کاری و امثال آن. تا پیش از این برای معرفی یک چنین تاریخ‌هایی، عموما قسمت زمان DateTime را با 00:00:00.000 مقدار دهی می‌کردیم؛ اما دیگر نیازی به این نوع تعاریف نیست و می‌توان مقصود خود را صریح‌تر بیان کرد.
روش معرفی نمونه‌ای از آن با معرفی سال، ماه و روز است:
 var date = new DateOnly(2020, 04, 20);
و یا اگر خواستیم یک DateTime موجود را به DateOnly تبدیل کنیم، می‌توان به صورت زیر عمل کرد:
 var currentDate = DateOnly.FromDateTime(DateTime.Now);

همچنین در اینجا نیز همانند DateTime می‌توان از متدهای Parse و یا TryParse، برای تبدیل یک رشته به معادل DateOnly آن، کمک گرفت:
if (DateOnly.TryParse("28/09/1984", new CultureInfo("en-US"), DateTimeStyles.None, out var result))
{
   Console.WriteLine(result);
}
در یک چنین حالتی ذکر CultureInfo، دقت کار را افزایش می‌دهد؛ در غیراینصورت از CultureInfo ترد جاری برنامه استفاده خواهد شد که می‌تواند در سیستم‌های مختلف، متفاوت باشد.

و یا می‌توان توسط متد ParseExact، ساختار تاریخ دریافتی را دقیقا مشخص کرد:
DateOnly d1 = DateOnly.ParseExact("31 Dec 1980", "dd MMM yyyy", CultureInfo.InvariantCulture);  // Custom format
Console.WriteLine(d1.ToString("o", CultureInfo.InvariantCulture)); // "1980-12-31"  (ISO 8601 format)

در حین نمونه سازی DateOnly، امکان ذکر تقویم‌های خاص، مانند PersianCalendar نیز وجود دارد:
var persianCalendar = new PersianCalendar();
DateOnly d2 = new DateOnly(1400, 9, 6, persianCalendar);
Console.WriteLine(d2.ToString("d MMMM yyyy", CultureInfo.InvariantCulture));

در اینجا همچنین متدهایی مانند AddDays، AddMonths و AddYears نیز بر روی date مهیا کار می‌کنند:
var newDate = date.AddDays(1).AddMonths(1).AddYears(1)

یک نکته: برخلاف DateTime، نوع DateOnly به همراه DateTimeKind مانند Utc و امثال آن نیست و همواره DateTimeKind آن Unspecified است.


روش استفاده از نوع TimeOnly در دات نت 6

نوع و ساختار TimeOnly، قسمت زمان را به نحو صریحی مشخص می‌کند؛ مانند ساعتی که باید هر روز راس آن، آلارمی به صدا درآید و یا جلسه‌ای تشکیل شود و یا وظیفه‌ای صورت گیرد. سازنده‌ی آن overload‌های قابل توجهی را داشته و می‌تواند یکی از موارد زیر باشد:
public TimeOnly(int hour, int minute)
public TimeOnly(int hour, int minute, int second)
public TimeOnly(int hour, int minute, int second, int millisecond)
برای نمونه برای نمایش 10:30 صبح، می‌توان به صورت زیر عمل کرد:
var startTime = new TimeOnly(10, 30);
در اینجا قسمت ساعت، 24 ساعتی تعریف شده‌است. بنابراین برای نمونه، ساعت 1 عصر را باید به صورت 13 قید کرد:
var endTime = new TimeOnly(13, 00, 00);

و یا برای مثال می‌توان این نمونه‌ها را از هم کم کرد:
var diff = endTime - startTime;
خروجی این تفاوت محاسبه شده، بر حسب TimeSpan است:
Console.WriteLine($"Hours: {diff.TotalHours}");
و یا با استفاده از متد الحاقی ToTimeSpan می‌توان یک TimeOnly را به TimeSpan معادلی تبدیل نمود:
TimeSpan ts = endTime.ToTimeSpan();

برای تبدیل قسمت زمان DateTime به TimeOnly، می‌توان از متد FromDateTime به صورت زیر استفاده کرد:
var currentTime = TimeOnly.FromDateTime(DateTime.Now);
و یا اگر بخواهیم یک DateOnly را به DateTime تبدیل کنیم، می‌توان از متد الحاقی ToDateTime به همراه ذکر قسمت زمان آن بر حسب TimeOnly کمک گرفت:
DateTime dt = date.ToDateTime(new TimeOnly(0, 0));
Console.WriteLine(dt);

و در این حالت اگر خواستیم بررسی کنیم که آیا زمانی بین دو زمان دیگر واقع شده‌است یا خیر، می‌توان از متد IsBetween استفاده نمود:
 var isBetween = currentTime.IsBetween(startTime, endTime);
Console.WriteLine($"Current time {(isBetween ? "is" : "is not")} between start and end");

در اینجا امکان مقایسه این نمونه‌ها، توسط عملگرهایی مانند < نیز وجود دارد:
var startTime = new TimeOnly(08, 00);
var endTime = new TimeOnly(09, 00);
 
Console.WriteLine($"{startTime < endTime}");

اگر نیاز به تبدیل رشته‌ای به TimeOnly بود، می‌توان از متد ParseExact به همراه ذکر ساختار مدنظر، استفاده کرد:
TimeOnly time = TimeOnly.ParseExact("5:00 pm", "h:mm tt", CultureInfo.InvariantCulture);  // Custom format
Console.WriteLine(time.ToString("T", CultureInfo.InvariantCulture)); // "17:00:00"  (long time format)


عدم پشتیبانی System.Text.Json از نوع‌های جدید DateOnly و TimeOnly

فرض کنید رکوردی را به صورت زیر تعریف کرده‌ایم که از نوع‌های جدید DateOnly و TimeOnly، تشکیل شده‌است:
public record DataTypeTest(DateOnly Date, TimeOnly Time);
اگر سعی کنیم نمونه‌ای از آن را به JSON تبدیل کنیم:
var date = DateOnly.FromDateTime(DateTime.Now);
var time = TimeOnly.FromDateTime(DateTime.Now);
var test = new DataTypeTest(date, time);
var json = JsonSerializer.Serialize(test);
با استثنای زیر مواجه خواهیم شد:
Serialization and deserialization of 'System.DateOnly' instances are not supported.

برای رفع این مشکل می‌توان ابتدا تبدیلگر ویژه‌ی DateOnly و
    public class DateOnlyConverter : JsonConverter<DateOnly>
    {
        private readonly string _serializationFormat;

        public DateOnlyConverter() : this(null)
        { }

        public DateOnlyConverter(string? serializationFormat)
        {
            _serializationFormat = serializationFormat ?? "yyyy-MM-dd";
        }

        public override DateOnly Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
        {
            var value = reader.GetString();
            return DateOnly.ParseExact(value!, _serializationFormat, CultureInfo.InvariantCulture);
        }

        public override void Write(Utf8JsonWriter writer, DateOnly value, JsonSerializerOptions options)
            => writer.WriteStringValue(value.ToString(_serializationFormat));
    }
و سپس تبدیلگر ویژه‌ی TimeOnly را به صورت زیر تدارک دید:
    public class TimeOnlyConverter : JsonConverter<TimeOnly>
    {
        private readonly string _serializationFormat;

        public TimeOnlyConverter() : this(null)
        {
        }

        public TimeOnlyConverter(string? serializationFormat)
        {
            _serializationFormat = serializationFormat ?? "HH:mm:ss.fff";
        }

        public override TimeOnly Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
        {
            var value = reader.GetString();
            return TimeOnly.ParseExact(value!, _serializationFormat, CultureInfo.InvariantCulture);
        }

        public override void Write(Utf8JsonWriter writer, TimeOnly value, JsonSerializerOptions options)
            => writer.WriteStringValue(value.ToString(_serializationFormat));
    }
و به نحو زیر مورد استفاده قرار داد:
var jsonOptions = new JsonSerializerOptions(JsonSerializerDefaults.Web);
jsonOptions.Converters.Add(new DateOnlyConverter());
jsonOptions.Converters.Add(new TimeOnlyConverter());
var json = JsonSerializer.Serialize(test, jsonOptions);
مطالب
آشنایی با جنریک‌ها #3
متدهای جنریک
متدهای جنریک، دارای پارامترهایی از نوع جنریک هستند و بوسیله‌ی آنها می‌توانیم نوع‌های (type) متفاوتی را به متد ارسال نمائیم. در واقع از متد، یک نمونه پیاده سازی کرده‌ایم، در حالیکه این متد را برای انواع دیگر هم می‌توانیم فراخوانی کنیم.

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

نحوه تعریف یک متد جنریک بشکل زیر است:
return-type method-name<type-parameters>(parameters)
قسمت مهم syntax بالا، type-parameters  است. در آن قسمت می‌توانید یک یا چند نوع که بوسیله کاما از هم جدا می‌شوند را تعریف کنید. این typeها در return-value و نوع برخی یا همه پارامترهای ورودی جنریک متد، قابل استفاده هستند. به کد زیر توجه کنید:
public T1 PrintValue<T1, T2>(T1 param1, T2 param2)
{
    Console.WriteLine("values are: parameter 1 = " + param1 + " and parameter 2 = " + param2);

    return param1;
}
در کد بالا، دو پارامتر ورودی بترتیب از نوع T1 و T2 و پارامتر خروجی (return-type) از نوع T1 تعریف کرده‌ایم.

اعمال محدودیت بر روی جنریک متدها
در زمان تعریف یک جنریک کلاس یا جنریک متد، امکان اعمال محدودیت بر روی typeهایی را که قرار است به آن‌ها ارسال شود، داریم. یعنی می‌توانیم تعیین کنیم جنریک متد چه typeهایی را در زمان ایجاد یک وهله‌ی از آن بپذیرد یا نپذیرد. اگر نوعی که به جنریک متد ارسال می‌کنیم جزء محدودیت‌های جنریک باشد با خطای کامپایلر روبرو خواهیم شد. این محدودیت‌ها با کلمه کلیدی where اعمال می‌شوند.
public void MyMethod< T >()
       where T : struct
{
  ...
}

محدودیت‌های قابل اعمال بر روی جنریک ها
  • struct: نوع آرگومان ارسالی باید value-type باشد؛ بجز مقادیر غیر NULL.
class C<T> where T : struct {} // value type
  • class: نوع آرگومان ارسالی باید reference-type (کلاس، اینترفیس، عامل، آرایه) باشد.
class D<T> where T : class {} // reference type
  • ()new: آرگومان ارسالی باید یک سازنده عمومی بدون پارامتر باشد. وقتی این محدوده کننده را با سایر محدود کننده‌ها به صورت همزمان استفاده می‌کنید، این محدوده کننده باید در آخر ذکر شود.
class H<T> where T : new() {} // no parameter constructor
public void MyMethod< T >()
       where T : IComparable, MyBaseClass, new ()
{
  ...
}
  • <base class name>: نوع آرگومان ارسالی باید از کلاس ذکر شده یا کلاس مشتق شده آن باشد.
class B {}
class E<T> where T : B {} // be/derive from base class
  • <interface name>: نوع آرگومان ارسالی باید اینترفیس ذکر شده یا پیاده ساز آن اینترفیس باشد.
interface I {}
class G<T> where T : I {} // be/implement interface
  • U: نوع آرگومان ارسالی باید از نوع یا مشتق شده U باشد.
class F<T, U> where T : U {} // be/derive from U
توجه: در مثال‌های بالا، محدوده کننده‌ها را برای جنریک کلاس‌ها اعمال کردیم که روش تعریف این محدودیت‌ها برای جنریک متدها هم یکسان است.

اعمال چندین محدودیت همزمان
برای اعمال چندین محدودیت همزمان بر روی یک آرگومان فقط کافی است محدودیت‌ها را پشت سرهم نوشته و آنها را بوسیله کاما از یکدیگر جدا نمایید.
interface I {}
class J<T>
  where T : class, I
در کلاس J بالا، برای آرگومان محدودیت class و اینترفیس I را اعمال کرده‌ایم.
این روش قابل تعمیم است:
interface I {}
class J<T, U>
  where T : class, I
  where U : I, new() {}
در کلاس J، آرگومان T با محدودیت‌های class و اینترفیس I و آرگومان U با محدودیت اینترفیس I و ()new تعریف شده است و البته تعداد آرگومان‌ها قابل گسترش است.
حال سوال این است: چرا از محدود کننده‌ها استفاده می‌کنیم؟
کد زیر را در نظر بگیرید:
//this method returns if both the parameters are equal 
public static bool Equals< T > (T t1, Tt2) 
{ 
  return (t1 == t2); 
}
متد بالا برای مقایسه دو نوع یکسان استفاده می‌شود. در مثال بالا در صورتیکه دو مقدار از نوع int با هم مقایسه نماییم جنریک متد بدرستی کار خواهد کرد ولی اگر بخواهیم دو مقدار از نوع string را مقایسه کنیم با خطای کامپایلر مواجه خواهیم شد. عمل مقایسه دو مقدار از نوع string که مقادیر در heap نگهداری می‌شوند بسادگی مقایسه دو مقدار int نیست. چون همانطور که می‌دانید int یک value-type و string یک reference-type است و برای مقایسه دو reference-type با استفاده از عملگر ==  تمهیداتی باید در نظر گرفته شود.
برای حل مشکل بالا 2 راه حل وجود دارد:
  1. Runtime casting
  2. استفاده از محدود کننده‌ها
casting در زمان اجرا، بعضی اوقات شاید مناسب باشد. در این مورد، CLR نوع‌ها را در زمان اجرا بدلیل کارکرد صحیح بصورت اتوماتیک cast خواهد کرد اما مطمئناً این روش همیشه مناسب نیست مخصوصاً زمانی که نوع‌های مورد استفاده در حال تحریف رفتار طبیعی عملگرها باشند (مانند آخرین نمونه بالا).
مطالب
Roslyn #5
بررسی Semantic Models

همانطور که از قسمت قبل به‌خاطر دارید، برای دسترسی به اطلاعات semantics، نیاز به یک context مناسب که همان Compilation API است، می‌باشد. این context دارای اطلاعاتی مانند دسترسی به تمام نوع‌های تعریف شده‌ی توسط کاربر و متادیتاهای ارجاعی، مانند کلاس‌های پایه‌ی دات نت فریم‌ورک است. بنابراین پس از ایجاد وهله‌ای از Compilation API، کار با فراخوانی متد GetSemanticModel آن ادامه می‌یابد. در ادامه با مثال‌هایی، کاربرد این متد را بررسی خواهیم کرد.


ساختار جدید Optional

خروجی‌های تعدادی از متدهای Roslyn با ساختار جدیدی به نام Optional ارائه می‌شوند:
    public struct Optional<T>
    {
        public bool HasValue { get; }
        public T Value { get; }
    }
این ساختار که بسیار شبیه است به ساختار قدیمی <Nullable<T، منحصر به Value types نیست و Reference types را نیز شامل می‌شود و بیانگر این است که آیا یک Reference type، واقعا مقدار دهی شده‌است یا خیر؟


دریافت مقادیر ثابت Literals

فرض کنید می‌خواهیم مقدار ثابت ; int x = 42 را دریافت کنیم. برای اینکار ابتدا باید syntax tree آن تشکیل شود و سپس نیاز به یک سری حلقه و if و else و همچنین بررسی نال بودن بسیاری از موارد است تا به نود مقدار ثابت 42 برسیم. سپس متد GetConstantValue مربوط به GetSemanticModel را بر روی آن فراخوانی می‌کنیم تا به مقدار واقعی آن که ممکن است در اثر محاسبات جاری تغییر کرده باشد، برسیم.
اما روش بهتر و توصیه شده، استفاده از CSharpSyntaxWalker است که در انتهای قسمت سوم معرفی شد:
class ConsoleWriteLineWalker : CSharpSyntaxWalker
{
    public ConsoleWriteLineWalker()
    {
        Arguments = new List<ExpressionSyntax>();
    }
 
    public List<ExpressionSyntax> Arguments { get; }
 
    public override void VisitInvocationExpression(InvocationExpressionSyntax node)
    {
        var member = node.Expression as MemberAccessExpressionSyntax;
        var type = member?.Expression as IdentifierNameSyntax;
        if (type != null && type.Identifier.Text == "Console" && member.Name.Identifier.Text == "WriteLine")
        {
            if (node.ArgumentList.Arguments.Count == 1)
            {
                var arg = node.ArgumentList.Arguments.Single().Expression;
                Arguments.Add(arg);
                return;
            }
        }
 
        base.VisitInvocationExpression(node);
    }
}
اگر به کدهای ادامه‌ی بحث دقت کنید، قصد داریم مقادیر ثابت آرگومان‌های Console.WriteLine را استخراج کنیم. به همین جهت در این SyntaxWalker، نوع Console و متد WriteLine آن مورد بررسی قرار گرفته‌اند. اگر این نود دارای یک تک آرگومان بود، آین آرگومان استخراج شده و به لیست آرگومان‌های خروجی این کلاس اضافه می‌شود.
در ادامه نحوه‌ی استفاده‌ی از این SyntaxWalker را ملاحظه می‌کنید. در اینجا ابتدا سورس کدی حاوی یک سری Console.WriteLine که دارای تک آرگومان‌های ثابتی هستند، تبدیل به syntax tree می‌شود. سپس از روی آن CSharpCompilation تولید می‌گردد تا بتوان به اطلاعات semantics دسترسی یافت:
static void getConstantValue()
{
    // Get the syntax tree.
    var code = @"
                using System;
 
                class Foo
                {
                    void Bar(int x)
                    {
                        Console.WriteLine(3.14);
                        Console.WriteLine(""qux"");
                        Console.WriteLine('c');
                        Console.WriteLine(null);
                        Console.WriteLine(x * 2 + 1);
                    }
                }
                ";
 
    var tree = CSharpSyntaxTree.ParseText(code);
    var root = tree.GetRoot();
 
    // Get the semantic model from the compilation.
    var mscorlib = MetadataReference.CreateFromFile(typeof(object).Assembly.Location);
    var comp = CSharpCompilation.Create("Demo").AddSyntaxTrees(tree).AddReferences(mscorlib);
    var model = comp.GetSemanticModel(tree);
 
    // Traverse the tree.
    var walker = new ConsoleWriteLineWalker();
    walker.Visit(root);
 
 
    // Analyze the constant argument (if any).
    foreach (var arg in walker.Arguments)
    {
        var val = model.GetConstantValue(arg);
        if (val.HasValue)
        {
            Console.WriteLine(arg + " has constant value " + (val.Value ?? "null") + " of type " + (val.Value?.GetType() ?? typeof(object)));
        }
        else
        {
            Console.WriteLine(arg + " has no constant value");
        }
    }
}
در ادامه با استفاده از CSharpCompilation و متد GetSemanticModel آن به SemanticModel جاری دسترسی خواهیم یافت. اکنون SyntaxWalker را وارد به حرکت بر روی ریشه‌ی syntax tree سورس کد آنالیز شده می‌کنیم. به این ترتیب لیست آرگومان‌های متدهای Console.WriteLine بدست می‌آیند. سپس با فراخوانی متد model.GetConstantValue بر روی هر آرگومان دریافتی، مقادیر آن‌ها با فرمت <Optional<T استخراج می‌شوند.
خروجی نمایش داده شده‌ی توسط برنامه به صورت ذیل است:
 3.14 has constant value 3.14 of type System.Double
"qux" has constant value qux of type System.String
'c' has constant value c of type System.Char
null has constant value null of type System.Object
x * 2 + 1 has no constant value


درک مفهوم Symbols

اینترفیس ISymbol در Roslyn، ریشه‌ی تمام Symbolهای مختلف مدل سازی شده‌ی در آن است که تعدادی از آن‌ها را در تصویر ذیل مشاهده می‌کنید:


API کار با Symbols بسیار شبیه به API کار با Reflection است با این تفاوت که در زمان آنالیز کدها رخ می‌دهد و نه در زمان اجرای برنامه. همچنین در Symbols API امکان دسترسی به اطلاعاتی مانند locals, labels و امثال آن نیز وجود دارد که با استفاده از Reflection زمان اجرای برنامه قابل دسترسی نیستند. برای مثال فضاهای نام در Reflection صرفا به صورت رشته‌ای، با دات جدا شده از نوع‌های آنالیز شده‌ی توسط آن است؛ اما در اینجا مطابق تصویر فوق، یک اینترفیس مجزای خاص خود را دارد. جهت سهولت کار کردن با Symbols، الگوی Visitor با معرفی کلاس پایه‌ی SymbolVisitor نیز پیش بینی شده‌است.
static void workingWithSymbols()
{
    // Get the syntax tree.
    var code = @"
                using System;
 
                class Foo
                {
                    void Bar(int x)
                    {
                        // #insideBar
                    }
                }
 
                class Qux
                {
                    protected int Baz { get; set; }
                }
                ";
 
    var tree = CSharpSyntaxTree.ParseText(code);
    var root = tree.GetRoot();
 
    // Get the semantic model from the compilation.
    var mscorlib = MetadataReference.CreateFromFile(typeof(object).Assembly.Location);
    var comp = CSharpCompilation.Create("Demo").AddSyntaxTrees(tree).AddReferences(mscorlib);
    var model = comp.GetSemanticModel(tree);
 
    // Traverse enclosing symbol hierarchy.
    var cursor = code.IndexOf("#insideBar");
    var barSymbol = model.GetEnclosingSymbol(cursor);
    for (var symbol = barSymbol; symbol != null; symbol = symbol.ContainingSymbol)
    {
        Console.WriteLine(symbol);
    }
 
    // Analyze accessibility of Baz inside Bar.
    var bazProp = ((CompilationUnitSyntax)root)
        .Members.OfType<ClassDeclarationSyntax>()
        .Single(m => m.Identifier.Text == "Qux")
        .Members.OfType<PropertyDeclarationSyntax>()
        .Single();
    var bazSymbol = model.GetDeclaredSymbol(bazProp);
    var canAccess = model.IsAccessible(cursor, bazSymbol);
}
یکی از کاربردهای مهم Symbols API دریافت اطلاعات Symbols نقطه‌ای خاص از کدها می‌باشد. برای مثال در محل اشاره‌گر ادیتور، چه Symbols ایی تعریف شده‌اند و از آن‌ها در مباحث ساخت افزونه‌های آنالیز کدها زیاد استفاده می‌شود. نمونه‌ای از آن‌را در قطعه کد فوق ملاحظه می‌کنید. در اینجا با استفاده از متد GetEnclosingSymbol، سعی در یافتن Symbols قرار گرفته‌ی در ناحیه‌ی insideBar# کدهای فوق داریم؛ با خروجی ذیل که نام demo.exe آن از نام CSharpCompilation آن گرفته شده‌است:
 Foo.Bar(int)
Foo
<global namespace>
Demo.exe
Demo, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null


همچنین در ادامه‌ی کد، توسط متد IsAccessible قصد داریم بررسی کنیم آیا Symbol قرار گرفته در محل کرسر، دسترسی به خاصیت protected کلاس Qux را دارد یا خیر؟ که پاسخ آن خیر است.


آشنایی با Binding symbols

یکی از مراحل کامپایل کد، binding نام دارد و در این مرحله است که اطلاعات Symbolic هر نود از Syntax tree دریافت می‌شود. برای مثال در اینجا مشخص می‌شود که این x، آیا یک متغیر محلی است، یا یک فیلد و یا یک خاصیت؟
مثال ذیل بسیار شبیه است به مثال getConstantValue ابتدای بحث، با این تفاوت که در حلقه‌ی آخر کار از متد GetSymbolInfo استفاده شده‌است:
static void bindingSymbols()
{
    // Get the syntax tree.
    var code = @"
                using System;
 
                class Foo
                {
                    private int y;
 
                    void Bar(int x)
                    {
                        Console.WriteLine(x);
                        Console.WriteLine(y);
 
                        int z = 42;
                        Console.WriteLine(z);
 
                        Console.WriteLine(a);
                    }
                }";
 
    var tree = CSharpSyntaxTree.ParseText(code);
    var root = tree.GetRoot();
 
    // Get the semantic model from the compilation.
    var mscorlib = MetadataReference.CreateFromFile(typeof(object).Assembly.Location);
    var comp = CSharpCompilation.Create("Demo").AddSyntaxTrees(tree).AddReferences(mscorlib);
    var model = comp.GetSemanticModel(tree);
 
    // Traverse the tree.
    var walker = new ConsoleWriteLineWalker();
    walker.Visit(root);
 
    // Bind the arguments.
    foreach (var arg in walker.Arguments)
    {
        var symbol = model.GetSymbolInfo(arg);
        if (symbol.Symbol != null)
        {
            Console.WriteLine(arg + " is bound to " + symbol.Symbol + " of type " + symbol.Symbol.Kind);
        }
        else
        {
            Console.WriteLine(arg + " could not be bound");
        }
    }
}
با این خروجی:
 x is bound to int of type Parameter
y is bound to Foo.y of type Field
z is bound to z of type Local
a could not be bound
در مثال فوق، با استفاده از Syntax Walker طراحی شده در ابتدای بحث که کار استخراج آرگومان‌های متدهای Console.WriteLine را انجام می‌دهد، قصد داریم بررسی کنیم، هر آرگومان به چه Symbol ایی بایند شده‌است و نوعش چیست؟ برای مثال Console.WriteLine اول که از پارامتر x استفاده می‌کند، نوع x مورد استفاده‌اش چیست؟ آیا فیلد است، متغیر محلی است یا یک پارامتر؟ این اطلاعات را با استفاده از متد model.GetSymbolInfo می‌توان استخراج کرد.
مطالب
آموزش Linq - بخش ششم: عملگرهای پرس و جو قسمت پنجم (پایانی)
عملگرهای اتصال (Join Operators)
• Join
• GroupJoin
• Zip

عملگر Join

این عملگر همانند inner join در SQL، دو مجموعه را بر اساس کلید‌های مرتبط که از طریق پارامترها  به آن ارسال می‌شوند، با یکدیگر ترکیب می‌کند.
در عملیات Join، یک توالی ورودی که به آن توالی خارجی (Outer Sequence) گفته می‌شود با یک توالی دیگر که به آن توالی داخلی (Inner Sequence) می‌گوییم، بر اساس کلید‌های مشخص شده، ترکیب شده و یک توالی خروجی تولید می‌شود.

بررسی پارامتر‌های عملگر Join:
public static IEnumerable<TResult> Join<TOuter,TInner,TKey,TResult>
(this IEnumerable<TOuter> outer,
IEnumerable<TInner> inner,
Func<TOuter,TKey> outerKeySelector,
Func<TInner,TKey> innerKeySelector,
Func<TOuter,TInner,TResult> resultSelector)
 • <Inner IEnumerable<TInner: نشان دهنده توالی داخلی می‌باشد.
 • Func<Touter,Tkey> outerKeySelector : عنصر کلید، در توالی خارجی
 • Func<Tinner,Tkey> innerKeySelector : عنصر کلید، در توالی داخلی
 • Func<Touter,Tinner,Tresult> resultSelector : یک عبارت Lambda است که ظاهر عناصر خروجی را مشخص می‌کند.

نکته
: بطور کلی T در پارامتر‌های بالا معرف Generic Type Parameter می‌باشد؛ T==>Type  (هر نوع داده‌ای که ما مشخص کنیم).
نکته : عملگر Join یک امضاء دیگر نیز دارد که اجازه مشخص کردن IEqualityComparer را می‌دهد.

کد زیر استفاده از عملگر Join را نشان می‌دهد. توجه داشته باشید که اعلان صریح نوع داده‌ها در عبارات Lambda نوشته شده، فقط برای روشن‌تر شدن فرآیند عملیات می‌باشد.
تعریف دو آرایه از کلاس‌های Recipe و  Review:
Recipe[] recipes =
{
   new Recipe {Id = 1, Name = "Mashed Potato"},
   new Recipe {Id = 2, Name = "Crispy Duck"},
   new Recipe {Id = 3, Name = "Sachertorte"}
};

// inner sequence
Review[] reviews =
{
   new Review {RecipeId = 1, ReviewText = "Tasty!"},
   new Review {RecipeId = 1, ReviewText = "Not nice :("},
   new Review {RecipeId = 1, ReviewText = "Pretty good"},
   new Review {RecipeId = 2, ReviewText = "Too hard"},
   new Review {RecipeId = 2, ReviewText = "Loved it"}
};

  var query = recipes // recipes توالی خارجی
.Join(reviews, // reviewsتوالی داخلی
  (Recipe outerKey) => outerKey.Id, // کلید انخاب شده از توالی خارجی
  (Review innerKey) => innerKey.RecipeId, // کلید انتخاب شده از توالی داخلی
  // نحوه قالب بندی خروجی
  (recipe, review) => recipe.Name + " - " + review.ReviewText);

foreach (string item in query)
{
   Console.WriteLine(item);
}
خروجی مثال بالا:
Mashed Potato - Tasty!
Mashed Potato - Not nice :(
Mashed Potato - Pretty good
Crispy Duck - Too hard
Crispy Duck - Loved it
ساده‌تر شده‌ی کد بالا:
var query =
recipes.Join
(reviews,
outerKey => outerKey.Id,
innerKey => innerKey.RecipeId,
(recipe, review) => recipe.Name + " - " + review.ReviewText);
همانطور که مشاهده می‌کنید در خروجی مثال بالا، عبارت Sachertorte مشاهده نمی‌شود. علت آن است که عملیات انجام شده، عملیات Left Join می‌باشد. بدین معنا که عناصری که در توالی خارجی هیچ عنصر متناظری در توالی داخلی ندارند، در توالی خروجی ظاهر نخواهند شد.

پیاده سازی توسط عبارت‌های جستجو
کلمه کلیدی Join، در زمان استفاده از روش عبارت‌های پرس و جو، مورد استفاده قرار گرفت. دستور Join در قسمت چهارم از این سری آموزشی بطور کامل بررسی شده است. کد زیر نحوه اجرای دستور Join را به روش عبارت‌های پرس و جو، نشان می‌دهد.
var query = from recipe in recipes
join review in reviews
on
recipe.Id equals review.RecipeId
select new //انواع بی نام
{
    RecipeName = recipe.Name,
    RecipeReview = review.ReviewText
};

foreach (var item in query)
{
    Console.WriteLine(item.RecipeName + " - " + item.RecipeReview);
}
نکته : بررسی‌ها نشان داده است که استفاده از دستور Join، به روش عبارت‌های پرس و جو نسبت به عملگر‌های پرس و جو، خوانایی بیشتری دارد.

عملگر GroupJoin
نحوه عملکرد عملگر GroupJoin، شبیه عملگر Join می‌باشد؛ با این تفاوت که خروجی حاصل از دستور GroupJoin، یک ساختار سلسله مراتبی می‌باشد. توالی خروجی، مجموعه‌ای  از گروه‌ها می‌باشد که هر گروه، تشکیل شده‌است از عناصر توالی درونی.

بررسی پارامتر‌های عملگر GroupJoin
• <Inner IEnumerable<TInner : نشان دهنده توالی داخلی
• Func<Touter,Tkey> outerKeySelector : عنصر کلید، در توالی خارجی
• Func<Tinner,Tkey> innerKeySelector : عنصر کلید، در توالی داخلی
• Func<Touter,Ienumerable<Tinner>,Tresult> resultSelector : قالب بندی گروه‌های تولید شده خروجی را مشخص می‌کند

کد زیر استفاده از عملگر GroupJoin را نشان می‌دهد :
// outer sequence
Recipe[] recipes =
{
   new Recipe {Id = 1, Name = "Mashed Potato"},
   new Recipe {Id = 2, Name = "Crispy Duck"},
   new Recipe {Id = 3, Name = "Sachertorte"}
};

// inner sequence
Review[] reviews =
{
   new Review {RecipeId = 1, ReviewText = "Tasty!"},
   new Review {RecipeId = 1, ReviewText = "Not nice :("},
   new Review {RecipeId = 1, ReviewText = "Pretty good"},
   new Review {RecipeId = 2, ReviewText = "Too hard"},
   new Review {RecipeId = 2, ReviewText = "Loved it"}
};

var query = recipes
.GroupJoin(
reviews,
(Recipe outerKey) => outerKey.Id,//outer key
(Review innerKey) => innerKey.RecipeId,//inner key
(Recipe recipe, IEnumerable<Review> rev )=>تعریف ساختار گروه‌ها new
{
  RecipeName = recipe.Name,
  Reviews = rev
}
);

foreach (var item in query)
{
   Console.WriteLine($"Reviews for {item.RecipeName}");
   foreach (var review in item.Reviews)
   {
      Console.WriteLine($" - {review.ReviewText}");
   }
}
خروجی مثال فوق:
Reviews for Mashed Potato
 - Tasty!
 - Not nice :(
 - Pretty good
Reviews for Crispy Duck
 - Too hard
 - Loved it
Reviews for Sachertorte
همانطور که مشاهده می‌کنید گروه "Sachertorte" در خروجی اضافه شده است؛ در صورتی که هیچ عضوی ندارد.

پیاده سازی توسط عبارت‌های جستجو
var query =
from recipe in recipes
join review in reviews on recipe.Id equals review.RecipeId
into reviewGroup
select new //انواع بی نام
{
  RecipeName = recipe.Name,
  Reviews = reviewGroup//کلیه بازخورد‌ها مرتبط با یک دستور غذایی
};
خروجی مثال فوق:
Reviews for Mashed Potato
 - Tasty!
 - Not nice :(
 - Pretty good
Reviews for Crispy Duck
 - Too hard
 - Loved it
Reviews for Sachertorte

عملگر Zip

عملگر Zip، رفتاری متفاوت نسبت به عملگر GroupJoin و Join دارد و هیچ آیتمی را به عنوان کلید، از دو توالی دریافت نمی‌کند. عملگر Zip همه عناصر دو توالی را یک به یک، به ترتیب کنار هم قرار می‌دهد. مثل زیپ در دنیای واقعی که لبه‌های دو طرف زیپ را به هم می‌رساند.
 public class Ingredient
{
  public string Name { get; set; }
  public int Calories { get; set; }
}

string[] names = { "Flour", "Butter", "Sugar" };
int[] calories = { 100, 400, 500 };
IEnumerable<Ingredient> ingredients =
names.Zip(calories, (name, calorie) =>
new Ingredient
{
  Name = name,
  Calories = calorie
});
foreach (var item in ingredients)
{
Console.WriteLine($"{item.Name} has {item.Calories} calories");
}
خروجی مثال بالا :
Flour has 100 calories
Butter has 400 calories
Sugar has 500 calories
نکته: اگر تعداد اعضای مجموعه‌ها برابر نباشد، اعضای اضافی نادیده گرفته می‌شوند.

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