- Web Essentials - Working with less compiler
- LESS in the .Net World
- LESS - Web Essentials
- Refactoring your CSS with LESS in Visual Studio Web Essentials
- Exploring LESS coding features inside Visual Studio - Web Essential
- How to integrate a Bootswatch theme into an ASP.NET Mvc app
- SharePoint + Twitter Bootstrap = Less Hack
سری آموزشی نوشتن یک کامپایلر با #C
Roslyn #1
سکوی کامپایلر دات نت یا Roslyn (با تلفظ «رازلین») بازنویسی مجدد کامپایلرهای VB.NET و #C توسط همین زبانها است. این سکوی کامپایلر به همراه یک سری کتابخانه و اسمبلی ارائه میشود که امکان آنالیز زبانهای مدیریت شده را به صورت مستقل و یا یکپارچهی با ویژوال استودیو، فراهم میکنند. برای نمونه در VS.NET 2015 تمام سرویسهای زبانهای موجود، با Roslyn API جایگزین و بازنویسی شدهاند. نمونههایی از این سرویسهای زبانها، شامل Intellisense و مرور کدها مانند go to references and definitions، به همراه امکانات Refactoring میشوند. به علاوه به کمک Roslyn میتوان یک کامپایلر و ابزارهای مرتبط با آن، مانند FxCop را تولید کرد و یا در نهایت یک فایل اسمبلی نهایی را از آن تحویل گرفت.
چرا مایکروسافت Roslyn را تولید کرد؟
پیش از پروژهی Roslyn، کامپایلرهای VB.NET و #C با زبان ++C نوشته شده بودند؛ از این جهت که در اواخر دههی 90 که کار تولید سکوی دات نت در حال انجام بود، هنوز امکانات کافی برای نوشتن این کامپایلرها با زبانهای مدیریت شده وجود نداشت و همچنین زبان محبوب کامپایلر نویسی در آن دوران نیز ++C بود. این انتخاب در دراز مدت مشکلاتی مانند کاهش انعطاف پذیری و productivity تیم کامپایلر نویس را با افزایش تعداد سطرهای کامپایلر نوشته شده به همراه داشت و افزودن ویژگیهای جدید را به زبانهای VB.NET و #C سختتر و سختتر کرده بود. همچنین در اینجا برنامه نویسهای تیم کامپایلر مدام مجبور بودند که بین زبانهای مدیریت شده و مدیریت نشده سوئیچ کنند و امکان استفادهی همزمان از زبانهایی را که در حال توسعهی آن هستند، نداشتند.
این مسایل سبب شدند تا در طی بیش از یک دهه، چندین نوع کامپایلر از صفر نوشته شوند:
- کامپایلرهای خط فرمانی مانند csc.exe و vbc.exe
- کامپایلر پشت صحنهی ویژوال استودیو (برای مثال کشیدن یک خط قرمز زیر مشکلات دستوری موجود)
- کامپایلر snippetها در immediate window ویژوال استودیو
هر کدام از این کامپایلرها هم برای حل مسایلی خاص طراحی شدهاند. کامپایلرهای خط فرمانی، با چندین فایل ورودی، به همراه ارائهی تعدادی زیادی خطا و اخطار کار میکنند. کامپایلر پشت صحنهی ویژوال استودیوهای تا پیش از نسخهی 2015، تنها با یک تک فایل در حال استفاده، کار میکند و همچنین باید به خطاهای رخ داده نیز مقاوم باشد و بیش از اندازه گزارش خطا ندهد. برای مثال زمانیکه کاربر در حالت تایپ یک سطر است، بدیهی است تا اتمام کار، این سطر فاقد ارزش دستوری صحیحی است و کامپایلر باید به این مساله دقت داشته باشد و یا کامپایلر snippetها تنها جهت ارزیابی یک تک سطر از دستورات وارد شده، طراحی شدهاست.
با توجه به این مسایل، مایکروسافت از بازنویسی سکوی کامپایلر دات نت این اهداف را دنبال میکند:
- بالا بردن سرعت افزودن قابلیتهای جدید به زبانهای موجود
- سبک کردن حجم کاری کامپایلر نویسی و کاهش تعداد آنها به یک مورد
- بالا بردن دسترسی پذیری به API کامپایلرها
برای مثال اکنون برنامه نویسها بجای اینکه یک فایل cs را به کامپایلر csc.exe ارائه کنند و یک خروجی باینری دریافت کنند، امکان دسترسی به syntax trees، semantic analysis و تمام مسایل پشت صحنهی یک کامپایلر را دارند.
- ساده سازی تولید افزونههای مرتبط با زبانهای مدیریت شده.
اکنون برای تولید یک آنالیز کنندهی سفارشی، نیازی نیست هر توسعه دهندهای شروع به نوشتن امکانات پایهای یک کامپایلر کند. این امکانات به صورت یک API عمومی در دسترس برنامه نویسها قرار گرفتهاند.
- آموزش مسایل درونی یک کامپایلر و همچنین ایجاد اکوسیستمی از برنامه نویسهای علاقمند در اطراف آن.
همانطور که اطلاع دارید، Roslyn به صورت سورس باز در GitHub در دسترس عموم است.
تفاوت Roslyn با کامپایلرهای سنتی
اکثر کامپایلرهای موجود به صورت یک جعبهی سیاه عمل میکنند. به این معنا که تعدادی فایل ورودی را دریافت کرده و در نهایت یک خروجی باینری را تولید میکنند. اینکه در این میان چه اتفاقاتی رخ میدهد، از دید استفاده کننده مخفی است.
نمونهای از این کامپایلرهای جعبه سیاه را در تصویر فوق مشاهده میکنید. در اینجا شاید این سؤال مطرح شود که در داخل جعبهی سیاه کامپایلر سیشارپ، چه اتفاقاتی رخ میدهد؟
خلاصهی مراحل رخ داده در کامپایلر سیشارپ را در تصویر فوق ملاحظه میکنید. در اینجا ابتدا کار parse اطلاعات متنی دریافتی شروع میشود و از روی آن syntax tree تولید میشود. در مرحلهی بعد مواردی مانند ارجاعاتی به mscorlib و امثال آن پردازش میشوند. در مرحلهی binder کار پردازش حوزهی دید متغیرها، اشیاء و اتصال آنها به هم انجام میشود. در مرحلهی آخر، کار تولید کدهای IL و اسمبلی باینری نهایی صورت میگیرد.
با معرفی Roslyn، این جعبهی سیاه، به صورت یک API عمومی در دسترس برنامه نویسها قرار گرفتهاست:
همانطور که مشاهده میکنید، هر مرحلهی کامپایل جعبهی سیاه، به یک API عمومی Roslyn نگاشت شدهاست. برای مثال Parser به Syntax tree API نگاشت شدهاست. به علاوه این API صرفا به موارد فوق خلاصه نمیشود و همانطور که پیشتر نیز ذکر شد، برای اینکه بتواند جایگزین سه نوع کامپایلر موجود شود، به همراه Workspace API نیز میباشد:
Roslyn امکان کار با یک Solution و فایلهای آن را دارد و شامل سرویسهای زبانهای مورد نیاز در ویژوال استودیو نیز میشود. برفراز Workspace API، یک مجموعه API دیگر به نام Diagnostics API تدارک دیده شدهاست تا برنامه نویسها بتوانند امکانات Refactoring جانبی را توسعه داده و یا در جهت بهبود کیفیت کدهای نوشته شده، اخطارهایی را به برنامه نویسها تحت عنوان Code fixes و آنالیز کنندهها، ارائه دهند.
همچنین مزیت دیگر آن، انتقال سادهتر کدهای جاوا به سیشارپ است؛ از این لحاظ که ویژگی مشابهی در زبان جاوا تحت عنوان «Default Methods» سالها است که وجود دارد.
یک مثال از ویژگی «پیاده سازیهای پیشفرض در اینترفیسها»
interface ILogger { void Log(string message); } class ConsoleLogger : ILogger { public void Log(string message) { Console.WriteLine(message); } }
مدتی بعد بر اساس نیازمندیهای مشخصی به این نتیجه خواهید رسید که بهتر است overload دیگری را برای متد Log در اینترفیس ILogger، درنظر بگیریم. مشکلی که این تغییر به همراه دارد، کامپایل نشدن کلاس ConsoleLogger در یک برنامهی ثالث است و این کلاس باید الزاما این overload جدید را پیاده سازی کند؛ در غیراینصورت قادر به کامپایل برنامهی خود نخواهد شد. اکنون در C# 8.0 میتوان برای این نوع تغییرات، در همان اینترفیس اصلی، یک پیاده سازی پیشفرض را نیز قرار داد:
interface ILogger { void Log(string message); void Log(Exception exception) => Console.WriteLine(exception); }
ویژگی «پیاده سازیهای پیشفرض در اینترفیسها» چگونه پیاده سازی شدهاست؟
واقعیت این است که امکان پیاده سازی این ویژگی، سالها است که در سطح کدهای IL دات نت وجود داشته (از زمان دات نت 2) و اکنون از طریق کدهای برنامه با بهبود کامپایلر آن، قابل دسترسی شدهاست.
تاثیر زمینهی کاری بر روی دسترسی به پیاده سازیهای پیشفرض
مثال زیر را درنظر بگیرید:
interface IDeveloper { void LearnNewLanguage(string language, DateTime dueDate); void LearnNewLanguage(string language) { // default implementation LearnNewLanguage(language, DateTime.Now.AddMonths(6)); } } class BackendDev : IDeveloper // compiles OK { public void LearnNewLanguage(string language, DateTime dueDate) { // Learning new language... } }
سؤال: به نظر شما اکنون کدامیک از کاربردهای زیر از کلاس BackendDev، کامپایل میشود و کدامیک خیر؟
IDeveloper dev1 = new BackendDev(); dev1.LearnNewLanguage("Rust"); var dev2 = new BackendDev(); dev2.LearnNewLanguage("Rust");
There is no argument given that corresponds to the required formal parameter 'dueDate' of 'BackendDev.LearnNewLanguage(string, DateTime)' (CS7036) [ConsoleApp]
ارثبری چندگانه چطور؟
احتمالا حدس زدهاید که این قابلیت ممکن است ارثبری چندگانه را که در سیشارپ ممنوع است، میسر کند. تا C# 8.0، یک کلاس تنها از یک کلاس دیگر میتواند مشتق شود؛ اما این محدودیت در مورد اینترفیسها وجود ندارد. به علاوه تاکنون اینترفیسها مانند کلاسها، امکان تعریف پیاده سازی خاصی را نداشتند و صرفا یک قرارداد بیشتر نبودند. بنابراین اکنون این سؤال مطرح میشود که آیا میتوان با ارائهی پیاده سازی پیشفرض متدها در اینترفیسها، ارثبری چندگانه را در سیشارپ پیاده سازی کرد؛ مانند مثال زیر؟!
using System; namespace ConsoleApp { public interface IDev { void LearnNewLanguage(string language) => Console.Write($"Learning {language} in a default way."); } public interface IBackendDev : IDev { void LearnNewLanguage(string language) => Console.Write($"Learning {language} in a backend way."); } public interface IFrontendDev : IDev { void LearnNewLanguage(string language) => Console.Write($"Learning {language} in a frontend way."); } public interface IFullStackDev : IBackendDev, IFrontendDev { } public class Dev : IFullStackDev { } }
IFullStackDev dev = new Dev(); dev.LearnNewLanguage("TypeScript");
The call is ambiguous between the following methods or properties: 'IBackendDev.LearnNewLanguage(string)' and 'IFrontendDev.LearnNewLanguage(string)' (CS0121)
تفاوت امکانات کلاسهای Abstract با متدهای پیشفرض اینترفیسها چیست؟
اینترفیسها هنوز نمیتوانند مانند کلاسها، سازندهای را تعریف کنند. نمیتوانند متغیرها/فیلدهایی را در سطح اینترفیس داشته باشند. همچنین در اینترفیسها همهچیز public است و امکان تعریف سطح دسترسی دیگری وجود ندارد.
بنابراین باید بخاطر داشت که هدف از تعریف اینترفیسها، ارائهی «یک رفتار» است و هدف از تعریف کلاسها، ارائه «یک حالت».
یک نکته: در نگارشهای پیش از C# 8.0 هم میتوان ویژگی «متدهای پیشفرض» را شبیه سازی کرد
واقعیت این است که توسط ویژگی «متدهای الحاقی»، سالها است که امکان افزودن «متدهای پیشفرضی» به اینترفیسها در زبان سیشارپ وجود دارد:
namespace MyNamespace { public interface IMyInterface { IList<int> Values { get; set; } } public static class MyInterfaceExtensions { public static int CountGreaterThan(this IMyInterface myInterface, int threshold) { return myInterface.Values?.Where(p => p > threshold).Count() ?? 0; } } }
var myImplementation = new MyInterfaceImplementation(); // Note that there's no typecast to IMyInterface required var countGreaterThanFive = myImplementation.CountGreaterThan(5);
یک MessageBox بهتر
موفق باشید!
Interceptor چیست؟
از زمان ارائهی NET 8 preview 6 SDK. به بعد، امکان رهگیری هر متدی از کدهای برنامه، به داتنت اضافه شدهاست؛ به همین جهت از واژهی Interceptor/رهگیر در اینجا استفاده میشود. خود تیم داتنت از این قابلیت در جهت بازنویسی پویای قسمتهایی از کدهای زیرساخت داتنت که از Reflection استفاده میکنند، با نگارشهای کامپایل شدهی مختص به برنامهی شما، کمک میگیرند. به این ترتیب سرعت و کارآیی برنامههای داتنت 8، بهبود قابل ملاحظهای را پیدا کردهاند. برای مثال ahead-of-time compilation (AOT) در داتنت 8 و ASP.NET Core 8x بر اساس این ویژگی پیاده سازی شدهاست. این ویژگی جدید، مکمل source generators است که در نگارشهای پیشین داتنت ارائه شده بود.
بررسی Interceptors با تهیهی یک مثال ساده
فرض کنید میخواهیم فراخوانی متد GetText زیر را رهگیری کرده و سپس آنرا با نمونهی دیگری جایگزین کنیم:
namespace CS8Tests; public class InterceptorsSample { public string GetText(string text) { return $"{text}, World!"; } }
namespace System.Runtime.CompilerServices; [AttributeUsage(AttributeTargets.Method, AllowMultiple = true, Inherited = false)] public sealed class InterceptsLocationAttribute : Attribute { public InterceptsLocationAttribute(string filePath, int line, int character) { } }
سپس فرض کنید فراخوانی متد GetText در فایل Program.cs برنامه به صورت زیر انجام شدهاست:
using CS8Tests; var example = new InterceptorsSample(); var text = example.GetText("Hello"); Console.WriteLine(text); //Hello, World!
در ادامه از این اطلاعات در رهگیر سفارشی زیر استفاده خواهیم کرد:
using System.Runtime.CompilerServices; namespace CS8Tests; public static class MyInterceptor { [InterceptsLocation("C:\\Path\\To\\CS8Tests\\Program.cs", 4, 20)] public static string InterceptorMethod(this InterceptorsSample example, string text) { return $"{text}, DNT!"; } }
اکنون اگر برنامه را اجرا کنیم ... با خطای زیر مواجه میشویم:
error CS9137: The 'interceptors' experimental feature is not enabled in this namespace. Add '<InterceptorsPreviewNamespaces>$(InterceptorsPreviewNamespaces);CS8Tests</InterceptorsPreviewNamespaces>' to your project.
<Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> <OutputType>Exe</OutputType> <TargetFramework>net8.0</TargetFramework> <ImplicitUsings>enable</ImplicitUsings> <Nullable>enable</Nullable> <!--<NoWarn>Test001</NoWarn>--> <InterceptorsPreviewNamespaces>$(InterceptorsPreviewNamespaces);CS8Tests</InterceptorsPreviewNamespaces> </PropertyGroup> </Project>
Hello, DNT!
سؤال: آیا رهگیری انجام شده، در زمان کامپایل انجام میشود یا در زمان اجرا؟
برای این مورد میتوان به Low-Level C# code تولیدی مراجعه کرد. برای مشاهدهی یک چنین کدهایی میتوانید از منوی Tools->IL Viewer برنامهی Rider استفاده کرده و در برگهی ظاهر شده، گزینهی Low-Level C# آنرا انتخاب نمائید:
using CS8Tests; using System; using System.Runtime.CompilerServices; [CompilerGenerated] internal class Program { private static void <Main>$(string[] args) { Console.WriteLine(new InterceptorsSample().InterceptorMethod("Hello")); } public Program() { base..ctor(); } }
سؤال: آیا این قابلیت واقعا کاربردی است؟!
اکنون شاید این سؤال مطرح شود که ... واقعا چه کسی قرار است مسیر کامل یک فایل، شماره سطر و شماره ستون فراخوانی متدی را به اینگونه در اختیار سیستم رهگیری قرار دهد؟! آیا واقعا این قابلیت، یک قابلیت کاربردی و مناسب است؟!
اینجا است که اهمیت source generators مشخص میشود. توسط source generators دسترسی کاملی به syntax trees وجود دارد و همچنین یکسری اطلاعات تکمیلی مانند FilePath و سپس CSharpSyntaxNodeها که دسترسی به دادههای متد ()GetLocation را دارند که مکان دقیق سطر و ستونهای فراخوانیها را مشخص میکند.
کاربردهای فعلی رهگیرها در دات نت 8
در دات نت 8، این موارد با استفاده از رهگیرها بهینه سازی شده و سرعت آنها افزایش یافتهاند:
- فراخوانیهایی که تمام اطلاعات آنها در زمان کامپایل فراهم است، مانند Regex.IsMatch(@"a+b+") که از یک الگوی ثابت و مشخص استفاده میکند، رهگیری شده و پیاده سازی آن با کدی استاتیک، جایگزین میشود.
- در ASP.NET Minimal API، استفاده از lambda expressions جهت ارائهی تعاریفی مانند:
app.MapGet("/products", handler: (int? page, int? pageLength, MyDb db) => { ... })
- بهبود کارآیی foreach loops جهت استفاده از ریاضیات برداری و SIMD در صورت امکان.
- بهبود کارآیی تزریق وابستگیها، زمانیکه به تعاریف مشخصی مانند ()<provider.Register<MyService ختم میشود.
- بجای استفاده از expression trees در زمان اجرای برنامه، اکنون میتوان کدهای SQL معادل را در زمان کامپایل برنامه تولید کرد.
- بهبود کارآیی Serializers، زمانیکه از یک نوع مشخص مانند ()<Serialize<MyType استفاده میشود و کامپایلر میتواند آنرا با کدهای زمان کامپایل، جایگزین کند.
محدودیتهای رهگیرها در داتنت 8
- رهگیرهای داتنت 8 فقط با متدها کار میکنند.
- مسیر ارائه شده حتما باید یک مسیر کامل و مشخص باشد. یعنی اگر این قطعه کد، به سیستم دیگری منتقل شود، کامپایل نخواهد شد و امکان ارائهی مسیرهای نسبی وجود ندارد.
- امضای متدها، حتما باید یکی باشد. یعنی نمیتوان یک رهگیر جنریک را تعریف کرد.
معرفی استاندارد سورس باز #C
Moving the standards work into the open, under the .NET Foundation, makes it easier for standardization work. Everything from language innovation and feature design through implementation and on to standardization now takes place in the open. It will be easier to ask questions among the language design team, the compiler implementers, and the standards committee. Even better, those conversations will be public.
لیستی از C# Source Generators
C# Source Generators
A list of C# Source Generators (not necessarily awesome), because I haven't found a good list yet.
C# Source Generators is a Roslyn compiler feature introduced in C#9/.NET 5. It lets C# developers inspect user code and generate new C# source files that can be added to a compilation.
نگاهی به ویژگی های جدید 6 #C
The C# language itself has changed little in version 6, the main importance of the release being the introduction of the Roslyn .NET Compiler Platform. However the New features and improvements that have been made to C# are welcome because they are aimed at aiding productivity. Paulo Morgado explains what they are, and how to use them.
DuoCode is an alternative compiler, powered by Microsoft® Roslyn, and integrated in Visual Studio.
It magically cross-compiles your C# 6.0 code into high-quality readable JavaScript code, enabling rapid development of web applications utilizing the extensive features of the C# language, the Visual Studio IDE, and the .NET Framework base class libraries.