نسخه 10 زبان محبوب سی شارپ (#C) در نوامبر سال گذشته میلادی همراه با نسخه 6 فریم ورک دات نت (NET.) عرضه شد. طبق زمان بندی ای که مایکروسافت از ارائه فریم ورک دات نت داده است، هر ساله در ماه نوامبر نسخه ای از فریم ورک دات نت عرضه می شود که همراه با آن، ویژگی های جدیدی هم به زبان سی شارپ اضافه خواهد شد. در این جا نگاهی خواهیم انداخت به ویژگی ها و امکاناتی که در نسخه 10 زبان سی شارپ به آن اضافه شده است.
ویژگی File scoped namespace
براساس Best Practiceی که اکثر توسعه دهندگان فریم ورک دات نت رعایت می کنند، هر یک از نوع هایی که در برنامه تعریف می شوند (مانند نوع Customer)، در یک فایل جدا قرار می گیرند و در هر فایل فقط یک namespace جهت قرار گرفتن آن نوع، درون آن تعریف شده و namespace های تو در تو هم استفاده نمی شود. مانند کد زیر:
using System;
namespace Store
{
public class Customer
{
public string Name { get; set; }
…
}
…
}
در چنین شرایطی، بدنه ای برای namespace تعریف شده و تعریف کلاس هم با یک تورفتگی درون آن قرار می گیرد. برای خواناتر کردن کد می توان انتهای تعریف namespace سیمی کالن قرار داد که نشان دهنده ی اتمام این دستور خواهد بود و در نتیجه می توان آکولادهای تعریف آن را حذف کرد. پس دو خط اضافی از کد و تو رفتگی اضافی تعریف کلاس حذف می شود و کد به این شکل در خواهد آمد.
using System;
namespace Store; // C# 10 file-scoped namespace
public class Customer
{
public string Name { get; set; }
}
به این ویژگی، File scoped namespace گفته می شود. اگر بخواهیم namespace دیگری را در این فایل تعریف کنیم با این خطا مواجه می شویم که نشان می دهد در سطح یک فایل فقط یک namespace قابل تعریف است.
Error CS8954: Source file can only contain one file-scoped namespace declaration
این ویژگی به دلیل حذف فضاهای عمودی زاید قبل از تعریف کلاس ها و استفاده ی بهینه از فضای افقی، سبب خواناتر شدن کد می شود.
ویژگی Global Using
به طور معمول در ابتدای هر فایل سی شارپ (cs.)، تعداد زیادی عبارت using وجود دارد که namespace ها را به این فایل import می کند. Using هایی مانند System یا System.Linq تقریبا در هر فایل سی شارپ استفاده شده و در همه ی فایل ها تکرار می شود. برای جلوگیری از تکرار این using ها، در نسخه 10 زبان سی شارپ ویژگی Global Using معرفی شده است. با این ویژگی برای import یک namespace، به جای using می توان از عبارت global using استفاده کرد. به این معنی که اگر در یکی از فایل های پروژه از global using استفاده کنید، در سایر فایل های همان پروژه، نیاز نخواهید داشت که مجدد همان using را بنویسید و در نتیجه از using ها فاکتور گرفته می شود.
شما می توانید global using را در یکی از فایل های اصلی پروژه مانند فایل Program.cs قرار دهید تا سایر فایل های پروژه از این using ها استفاده کنند، اما Best Practice این است که فایلی با نام GlobalNamespace.cs در پروژه اضافه کرده و using هایی که تقریبا در سطح پروژه از آن استفاده می کنید را در آن قرار بدهید مانند کد زیر:
global using System;
global using System.Linq;
global using System.Collections.Generic;
به این نکته توجه کنید امکان نوشتن using و global using با هم در یک فایل وجود دارد و هم چنین می توانید از global using برای import کلاس های static هم استفاده کنید یعنی به این شکل:
global using static System.Console;
ویژگی Implicit Using
پروژه هایی که با دات نت 6 توسعه داده می شوند، قابلیتی در خود دارند به نام Implicit Using که می توان آن را روی پروژه، فعال یا غیر فعال کرد. اگر این قابلیت در پروژه فعال باشد به صورت پیش فرض، یکسری using براساس نوع پروژه به آن اضافه می شود. مثلا اگر پروژه Console Application داشته باشید using های زیر به صورت پیش فرض به کل فایل های پروژه اضافه خواهد شد.
// <auto-generated/>
global using global::System;
global using global::System.Collections.Generic;
global using global::System.IO;
global using global::System.Linq;
global using global::System.Net.Http;
global using global::System.Threading;
global using global::System.Threading.Tasks;
روش فعال کردن آن به یکی از دو روش زیر انجام می گیرد.
همان طور که در تصویر زیر می بینید، اگر به بخش Properties پروژه در Solution Explorer در ویژوال استدیو بروید، گزینه ای اضافه شده با عنوان Implicit global usings که می توانید فعال یا غیر فعال کنید که با تیک زدن آن using ها به پروژه اضافه خواهد شد.
روش دیگر این است که مستقیم فایل csproj. پروژه (فایل تنطیمات پروژه) را باز کنید و تگ ImplicitUsings را با مقدار enable درون آن قرار دهید و یا برای غیرفعال کردن آن، مقدار disable را درون آن قرار دهید. مانند زیر:
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net6.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
</Project>
اگر می خواهید ببینید چه using هایی به صورت پیش فرض اضافه شده است، پروژه را بیلد (build) کرده و به مسیر obj\debug\net6.0 بروید. آن جا فایلی ساخته می شود با نام GlobalUsings.g.cs که using ها درون آن قرار داده شده است. به این مورد دقت کنید که کامپایلر این فایل را به صورت خودکار برای پروژه هایی که روی دات نت 6 توسعه داده می شوند و این قابلیت در آن ها فعال شده باشد، می سازد.
اگر می خواهید مواردی را به using های پیش فرض اضافه کنید باید تگ زیر را به تگ های فایل csproj. اضافه کنید.
<ItemGroup>
<Using Include="System.Numerics" />
</ItemGroup>
در این جا using مربوط به System.Numerics به موارد پیش فرض اضافه خواهد شد.
ویژگی Constant interpolated strings
در نسخه 6 زبان سی شارپ ویژگی interpolated strings معرفی شد. با استفاده از این ویژگی، می توان متغیر ها و عبارت های محاسباتی را با سایر رشته ها با روش خوانا تری به هم الحاق کرد. قبل از معرفی این ویژگی، باید از عملگر + برای عملیات string concatenation یا متد Format کلاس String استفاده می کردیم. به طور مثال اگر بخواهیم از الحاق دو متغیر firstName و lastName رشته جدیدی بسازیم باید این گونه عمل کنیم.
var firstName = "Ali";
var lastName = "Alavi";
var fullName = firstName + " " + lastName;
var fullname = string.Format("{0} {1}", firstName, lastName);
اما با استفاده از ویژگی interpolated strings، کاراکتر $ را قبل از رشته ی جدید می آوریم و داخل کروشه اسم متغیر یا عملیات محاسباتی را قرار می دهیم.
var fullName = $"{firstName} {lastName}";
این روش فرمت بندی رشته ها، خوانایی کد را افزایش می دهد. تا قبل از نسخه 10 زبان سی شارپ، این امکان وجود نداشت که نتیجه interpolated strings را در ثابت های رشته ای بریزیم و حتما باید نتیجه در یک متغیر قرار می گرفت. اما در نسخه 10 می توانیم نتیجه interpolated strings را در ثابت رشته ای هم قرار دهیم که به این ویژگی Constant interpolated strings گفته می شود.
private const string firstname = "Reza";
private const string lastname = "Bahrami";
private const string fullname = $"{ firstname } { lastname }";
دقت داشته باشید تنها ثابت هایی از نوع رشته را می توانید با استفاده از این روش به هم الحاق کنید و نتیجه را در یک ثابت رشته ای قرار داد و برای نوع های دیگر با خطا مواجه می شوید. در کد زیر چون از ثابت عدد صحیح استفاده شده است، کامپایل نخواهد شد و با خطا مواجه خواهید شد.
const string firstName = "salam";
const int age = 12;
const string fullName = $"{firstName} has {age} years";
Error CS0133: The expression being assigned to 'fullName' must be constant
امکان دیگری که این ویژگی به ما می دهد این است که می توانیم از ثابت های رشته ای در interpolated strings ها استفاده کنیم. مثلا فرض کنید متدی داریم که Attribute Obsolete روی آن قرار داده شده است و می خواهیم توضیحی برایش بنویسیم که چرا نباید از این متد استفاده کرد، در این توضیح می توان از Constant interpolated strings استفاده کرد و داخل آن از یک ثابت رشته ای هم استفاده کرد.
const string ObsoleteDesc = "This method is Obsolete";
[Obsolete($"{ObsoleteDesc}")]
void ObsoletePrint()
{
…
}
تغییرات Lambda Expression در نسخه 10 سی شارپ
در نسخه 3 سی شارپ کلمه کلیدی var اضافه شد که با استفاده از آن، کامپایلر از روی مقادیری که به یک متغیر نسبت داده می شود، نوع متغیر را تشخص داده و لازم نیست دقیقا نوع متغیر را اعلام کنیم. قبل از نسخه 10 سی شارپ اگر کد زیر را می نوشتیم به دلیل اینکه کامپایلر نمی توانست نوع delegate را تشخیص دهد با خطا مواجه می شدیم.
var pringHelloDelegate = () => Console.WriteLine("Hello");
پس برای رفع خطا باید دقیقا نوع delegate را مشخص کنیم.
Action pringHelloDelegate = () => Console.WriteLine("Hello");
اما در نسخه 10 ویژگی Inferred delegate type به آن اضافه شده است که کامپایلر این توانایی را پیدا کرده است که نوع delegate را تشخیص بدهد. پس می توانیم از var استفاده کنیم.
نکته ای که باید به آن توجه داشته باشیم این است که باید عبارت سمت راست تساوی، یعنی Lambda Expression را طوری بنویسیم که کامپایر بتواند آن را تشخیص دهد. مثلا فرض کنید این عبارت را بنویسیم.
var multi = x => x * x;
در این حالت کامپایلر چون نمی تواند نوع x را تشخیص دهد خطای زیر را صادر می کند.
Error CS8917 :The delegate type could not be inferred.
برای رفع خطا باید نوع x را دقیقا اعلام کنیم.
var multi = (int x) => x * x;
نکته ی دیگری که باید به آن توجه کنید، نوع خروجی delegate است. اگر delegate ی که می نویسیم دو نوع متفاوت را برگرداند، کامپایلر نوع خروجی را نمی تواند تشخیص داده و خطای بالا را نمایش می دهد. به عنوان مثال کد زیر را در نظر بگیرید.
var result = (int x) => x > 10 ? 20 : "twenty";
در این کد نوع خروجی براساس مقدار x، یکی از انواع int یا string خواهد بود. پس کامپایلر قادر به تشخیص خروجی نیست و خطا نمایش می دهد. در این نسخه از زبان سی شارپ، امکان تعیین نوع خروجی به Lambda Expression اضافه شده است. برای این کار، قبل از ورودی های Lambda Expression نوعی را تعیین می کنیم که مشخص کننده نوع خروجی Lambda Expression خواهد بود. به این صورت
var result = object (int x) => x > 10 ? 20 : "twenty";
در این جا تعیین کرده ایم که نوع خروجی Lambda Expression از نوع object خواهد بود. در نتیجه می توانیم هر نوعی را در خروجی داشته باشیم و کامپایلر هم از روی نوع گفته شده، نوع خروجی delegate را تشخص خواهد داد. به این ویژگی Lambda return type گفته می شود.
تعریف کردن Attribute روی Lambda Expression ها ويژگی دیگری است که به آن ها اضافه شده است. مانند متد های معمولی می توان به Lambda Expression ها هم Attribute اضافه کرد که به این ویژگی Lambda attribute گفته می شود. برای مثال می خواهیم Attribute Obsolete را به Lambda Expression اضافه کنیم پس به این صورت عمل می کنیم.
var result = [Obsolete] object (int x) => x > 10 ? 20 : "twenty";
اضافه شدن دو نوع جدید DateOnly و TimeOnly
به احتمال زیاد با نوع DateTime آشنایی دارید. همان طور که می دانید نوع DateTime، اطلاعات زمان و تاریخ را با هم در خود نگه می دارد. اگر مقدار property، DateTime.Now را در خروجی چاپ کنیم خواهید دید تاریخ و زمان جاری را به ما خواهد داد.
1/6/2022 5:51:51 PM
قبل از نسخه 10 سی شارپ، اگر فقط با یکی از دو بخش تاریخ یا زمان کار داشتیم، باید با نوع DateTime کار می کردیم و از یکی از دو بخش زمان یا تاریخ، صرف نظر می کردیم. اما در نسخه 10 دو نوع جدید به نام های DateOnly و TimeOnly اضافه شده است تا تاریخ و زمان را از هم جدا کند. به مثال زیر دقت کنید.
DateOnly dateOnly = DateOnly.FromDateTime(DateTime.Now);
TimeOnly timeOnly = TimeOnly.FromDateTime(DateTime.Now);
Console.WriteLine(dateOnly);
Console.WriteLine(timeOnly);
در خروجی خواهیم داشت.
1/6/2022
6:07 PM
همان طور که می بینید مقادیر تاریخ و زمان در دو شی جدا از هم نگه داشته می شوند و dateOnly فقط تاریخ را در خود نگه می دارد و timeOnly فقط زمان را. یکی از مزیت های جدا شدن این دو نوع را باید در انطباق بهتر این انواع با نوع های date و time در SQL Server دانست، چون از نظر داده به هم شباهت نزدیک تری پیدا کرده اند. البته باید به این نکته هم اشاره کرد این دو نوع به دات نت 6 اضافه شده است و آن را باید از امکانات فریم ورک دانست تا زبان سی شارپ.
بهبود عملکرد struct در نسخه 10 سی شارپ
struct ها در نسخه 10 زبان سی شارپ تغییراتی داشته اند. یکی از این تغییرات، امکان تعریف سازنده بدون پارامتر در struct است که تا قبل از این نسخه، امکان تعریف سازنده بدون پارامتر وجود نداشت و سازنده ی یک struct حتما باید حداقل یک پارامتر داشته باشد. به این ویژگی parameterless struct constructors گفته می شود. Struct زیر را در نظر بگیرید.
struct Point
{
public Point()
{
X = 0;
Y = 0;
}
public double X { get; set; }
public double Y { get; set; }
}
همان طور که در قطعه کد بالا می بینید، Point به عنوان یک struct، دارای یک سازنده بدون پارامتر است. باید توجه داشته باشید که حتما داخل سازنده، تمام property های آن را مقدار دهی کنید در غیر این صورت با خطای زیر مواجه می شوید.
Auto-implemented property 'Point.Y' must be fully assigned before control is returned to the caller.
همان طور که در متن خطا می بینید چون Y مقدار دهی نشده است با خطا مواجه شده ایم.
امکان دیگری که در نسخه 10 سی شارپ به struct ها اضافه شده است struct field initializers نام دارد. با استفاده از این ویژگی می توان در هنگام تعریف property، آن را مقداردهی کرد. به این صورت
public double X { get; set; } = 0;
با این کار خطای بالا هم رفع می شود چون property های struct همگی مقداردهی شده اند. به این نکته توجه داشته باشید که اگر از این امکان استفاده می کنید، باید در آخر تعریف property، از سیمی کالن استفاده کنید اما در حالت عادی این کار نیاز نیست.
امکان جذاب دیگری که به struct ها اضافه شده است with on structs نام دارد. با استفاده از این ویژگی می توان از شی ساخته شده struct، یک کپی گرفت و در صورت تمایل، بعضی از property های آن را هم تغییر داد.
var p1 = new Point(12, 13);
var p2 = p1 with { Y = 49 };
در مثال بالا شی p1 با مقادیر 12 برای X و 13 برای Y تعریف شده است. در خط دوم با استفاده از کلمه کلیدی with، یک Clone یا یک کپی از p1 گرفته شده و مقدار Y را به 49 تغییر داده است و به این ترتیب شی جدید p2 ساخته می شود. در صورت تمایل می توانید هر یک از property های آن را تغییر دهید یا اصلا هیچ کدام را تغییر ندهید تا یک کپی کامل گرفته شود.
تغییرات Property Pattern در نسخه 10 سی شارپ
در نسخه 8 سی شارپ، قابلیت property pattern معرفی شد. با استفاده از این قابلیت، بررسی و مقایسه کردن اشیا با مقادیر property هایشان، خواناتر و آسان تر شد. فرض کنید کلاس person و suit را داریم و می خواهیم متدی بنویسیم و در آن بررسی کنیم که آیا یک شخص دارای suit ی با رنگ قرمز است یا خیر. پس قطعه کد های زیر را می نویسیم.
public class Person
{
public string Name { get; set; }
public Suit? Suit { get; set; }
}
public class Suit
{
public string Color { get; set; }
}
static bool HasPersonRedSuit(Person person) => person is { Suit: { Color : "Red"} };
سپس یک شی از کلاس person ساخته و متد HasPersonRedSuit را روی آن فراخوانی می کنیم.
var ali = new Person()
{
Name = "Ali",
Suit = new Suit
{
Color = "Red",
}
};
Console.WriteLine(HasPersonRedSuit(ali));
خروجی این کد true خواهد بود.
این قسمت از کد با استفاده از ویژگی property pattern نوشته شده است.
person is { Suit: { Color : "Red"}
همان طور که می بینید suit ،property شی person بررسی می شود و اگر Color ،property آن برابر با red باشد true بر می گرداند و در غیر این صورت false بر می گرداند. در این نحوه ی نوشتن، برای بررسی مقدار هر property، مجبوریم یک جفت آکولاد باز و بسته بنویسیم. در نسخه 10 سی شارپ بهبودی در این ویژگی انجام شده است که نحوه نوشتن آن را خواناتر و آسان تر کرده است. خط بالا را می توان به این صورت باز نویسی کرد.
static bool HasPersonRedSuit(Person person) => person is { Suit.Color : "Red" }
آکولادهای (curly braces) اطراف Color ،property را حذف کرده و از عملگر "." برای دسترسی به آن استفاده خواهیم کرد که نسبت به روش قبل خوانا تر شده است.
امیدوارم پروژه های خود را به دات نت 6 به روز رسانی کرده و از این ویژگی های جدید هم استفاده کنید.