- Windows 11 SDK support.
- Adds Xcode 13.0 support.
- Add AMD64 math functions to ARM64X CRT.
- Updates to the ARM64 and ARM64EC interfaces between the binary and the POGO instrumentation runtime.
- Fixed several problems with IntelliSense responsiveness and correctness affecting C++20 concepts, ranges, and abbreviated function templates.
- Fixed a false positive in local lifetime checks.
- Corrected an issue where arrays allocated with a constant of size > 32bits could allocate less memory than requested.
- Ensures that ATL string initialization occurs during static variable initialization, in the default AppDomain.
- Fixed a bug in C++ Concurrency::parallel_for_each that was crashing the calling process due to integer overflow.
- Fixed a bug in the STL's iterator debugging machinery that could cause crashes in multithreaded programs using STL containers.
- We have fixed a fatal internal compiler error caused by unnamed structs whose fields are referenced from SAL annotations.
- Fixes a rare crash when analyzing templated code that uses __uuidof.
- Fixed an issue that caused C++ static analysis results to sometimes not display correctly in the FixIt action.
- Fixed opening .uitest extension files in Coded UI project
- Fire component change events for non-component objects also in WinForms .NET designer
- Fix for crash on deleting ContextMenuStrip control in Windows Forms .NET designer.
- Guard against crashes when the Windows Forms designer reloads when dragging.
- Fix for intermittent VS crash while interacting with WinForms .NET designer during solution or project rebuild.
- Fixed a bug causing .NET 5 projects to be reported as out of date when they should have been up to date, causing slower builds.
- Automatically disable asset-indexing for large scale Unity projects.
- This release fixes an issue with deploying certain Windows Application Packaging projects where deployment is unnecessarily copying unmodified files.
Bootstrap 3.4.0 منتشر شد
NET Core 3.0 Preview 9. منتشر شد
.NET Core 3.0 is supported with Visual Studio 2019 16.3 Preview 3 and Visual Studio for Mac 8.3, which were also released today.
معرفی C# Source Generators
مخلص کلام اینکه : اگه با Fody یا PostSharp و همچنین Code Analyzerها آشنایی دارین. این قابلیت یه چیزی تو مایههای ترکیب ایناس و بهتون اجازه میده موقع Compile شدن کد پروژه رو Analyze کنین و یه کد جدیدی بهش اضافه کنین. (مثلا پیاده سازی اینترفیس INotifyPropertyChanged به صورت خودکار به هنگام کامپایل)
We’re pleased to introduce the first preview of Source Generators, a new C# compiler feature that lets C# developers inspect user code and generate new C# source files that can be added to a compilation. This is done via a new kind of component that we’re calling a Source Generator.
To get started with Source Generators, you’ll need to install the latest .NET 5 preview and the latest Visual Studio preview.
نمونه ای از پیاده سازی INotifyPropertyChanged with C# 9.0 Source Generators
معرفی برخی عملگرها
در مقالات قبلی مقدماتی را جهت ورود به برنامه نویسی شیء گرا در جاوا اسکریپت مطرح کردیم و در اینجا نیز به معرفی برخی عملگرها میپردازیم که در برنامه نویسی شیء گرا نقشی اساسی را ایفا میکنند.
عملگر typeof
از آنجائیکه جاوا اسکریپت دارای نوع دادهای ضعیف یا Loosely Typed میباشد، باید در بکارگیری متغیرها و یا آرگومانهای ورودی توابع، دقت لازم را داشته باشیم تا خطایی در اجرای کد یا محاسبات به وجود نیاید. بنابراین به راهکارهایی نیاز داریم تا بتوانیم نوع دادهای یک متغیر را تشخیص دهیم و قبل از بکارگیری آنها صحت و اعتبار دادههای ورودی را بررسی کنیم. با استفاده از عملگر typeof میتوانیم نوع دادهای یک متغیر را تشخیص دهیم که برای هر نوع دادهای مقادیر زیر را بر میگرداند:
· برای متغیرهایی که شامل مقدار undefined میباشند مقدار "undefined"
· برای متغیرهای منطقی یا Boolean مقدار "boolean"
· برای متغیرهای رشتهای یا String مقدار "string"
· برای متغیرهای عددی و مقادیر NaN و Infinity مقدار "number"
· برای تابع مقدار "function"
· برای اشیا و مقادیر null مقدار "object"
var x; var n = 12; var obj = {}; var fn = function () { }; var a = new Array(); alert(typeof x); // "undefined" alert(typeof n); // "number" alert(typeof obj); // "object" alert(typeof fn); // "function" alert(typeof a); // "object"
عملگر instanceof
عملگر typeof بهترین روش جهت تشخیص نوع دادهای متغیرهایی است که دارای نوع دادهای پایه یا Primitive Type هستند. اما جهت تشخیص نوع دادهای اشیاء و به صورت کلی انواع ارجاعی، این عملگر فقط مقدار "object" را برمیگرداند و اشارهای به ماهیت واقعی آن Object ندارد. برای این منظور میتوانیم از عملگر instanceof استفاده نماییم تا بررسی کنیم یک نوع ارجاعی از جنس چه نوع Object ی میباشد. شکل کلی استفاده از این عملگر به صورت زیر است:
result = variable instanceof constructor
اگر variable ، از جنس نوع ارجاعی تعیین شده در بخش سازنده یا constructor باشد، عملگر instanceof مقدار true را بر میگرداند. به مثال زیر توجه کنید:
var a = new Array(); alert(a instanceof Array); // true alert(a instanceof Object); // true alert(a instanceof Date); // false
عملگر in
همانطور که قبلا اشاره شد، جهت دسترسی به اعضای یک شیء، میتوان با آن شیء همانند یک آرایه رفتار نمود. به عبارتی دیگر میتوان نام یک ویژگی یا تابع را در [] قرار داد تا به مقدار آن دسترسی داشت. بنابراین میتوان همانند یک آرایه و با استفاده از یک حلقهی for-in تمامی اعضای یک شیء را پیمایش نمود. در واقع عملگر in در این حلقه بررسی میکند چه ویژگیها و توابعی در یک شیء وجود دارند و تمامی آنها را بر میگرداند. به مثال زیر توجه کنید:
var person = { name: "Meysam", age: 33, sayInfo: function () { alert(name + ":" + age); } }; for (var i in person) alert(i + " => " + person[i]);
خروجی :
name => Meysam age => 33 sayInfo => function() { alert(name + ":" + age); }
کاربرد دیگر عملگر in بررسی وجود یک ویژگی یا تابع در یک شیء میباشد. اگر ویژگی یا تابع مورد نظر در شیء وجود داشته باشد، مقدار true را بر میگرداند. به مثال زیر توجه کنید:
alert("name" in person); // true alert("sayInfo" in person); // true alert("birth" in person); // false
عملگر delete
از عملگر delete جهت حذف یک ویژگی و یا یک تابع از یک شیء استفاده میشود. به مثال زیر توجه کنید:
var person = { name: "Meysam", age: 33, sayInfo: function () { alert(name + ":" + age); } }; alert("sayInfo" in person); // true delete person.sayInfo; alert("sayInfo" in person); // false
ویژگی constructor
پس از عملگرهای فوق، یکی از پرکاربردترین ویژگیهایی که برای اشیاء وجود دارد، ویژگی constructor میباشد. در واقع این ویژگی نیز یکی از راهکارهای بررسی صحت و اعتبار متغیرها، آرگومانها و اشیا میباشد. ویژگی constructor ، به تابع سازندهی یک شیء اشاره میکند و آن سازنده را به عنوان خروجی بر میگرداند. دقت داشته باشید که خروجی این ویژگی، خود تابع سازنده میباشد و یک مقدار رشتهای نیست. به مثال زیر توجه کنید:
var obj = {}; var a = new Array(); var x = 10; alert(obj.constructor); alert(obj.constructor === Object); alert(typeof obj.constructor); alert(a.constructor); alert(x.constructor);
خروجی :
function Object() { [native code] } true function function Array() { [native code] } function Number() { [native code] }
در اینجا دیگر آمادهی ورود به برنامه نویسی شیء گرا در جاوا اسکریپت میباشیم که در مقالات بعدی به آن خواهیم پرداخت و همچنین با جزئیات بیشتری اشیاء را تشریح مینماییم.
Dynamic Semantics
Objectها علاوه بر داده و رفتار به عنوان توصیفات ثابت، در زمان اجرا دارای یک Local State (a snapshot) از مقادیر داینامیک مربوط به اعضای دادهای خود، میباشند. مجموعه تمام حالاتی که وهلههای یک کلاس میتوانند بین آنها گذر (transition) داشته باشد، dynamic semantics مربوط به کلاس نامیده میشود و به وهلههای کلاس این امکان را میدهند تا به یک پیغام مشابه رسیده و در زمانهای مختلف از چرخه زندگی خود، به اشکال مختلف پاسخ دهند.
Method junk for the class X if (local state #1) then do something else if (local state #2) then do something different End Method
بخش اصلی هر طراحی شیء گرا، dynamic semantics وهلهها میباشد. dynamic semantics هر کلاسی باید در قالب یک دیاگرام state-transition مستند شود. شکل زیر dynamic semantics پروسههای موجود در یک سیستم عامل را در قابل یک دیاگرام حالت نمایش میدهد. این پروسهها توانایی این را دارند که در هر کدام از حالات: runnable، current process، blocked، sleeping و یا در حالت exited، قرار داشته باشند. همچنین به عنوان مثال، یک پروسه زمانی میتواند در حالت current process قرار گیرد که حتما قبلا در حالت runnable قرار داشته باشد. این اطلاعات برای ایجاد تست برای کلاسها و وهلههای آنها میتواند مفید واقع شود.
شکل 2.8 State-transition diagram notation
برخی از طراحان به طور تصادفی، dynamic semantics یک کلاس را به عنوان static semantics آن کلاس مدل میکنند. به عنوان مثال اگر color یکی از اعضای داده ای (data member) کلاس توپ باشد و بعد از وهله سازی از کلاس توپ، color آن بازهم قابل تغییر باشد، منظور اینکه توپ آبی به عنوان یک وهله از کلاس توپ در زمان حیات خود تغییر رنگ دهد، اصطلاحا میگویند: color جزء dynamic semantics کلاس توپ میباشد. با توجه به توضحیاتی که داده شد، حال اگر طراحی برای هر رنگ توپ یک کلاس جدا در نظر گرفته باشد، dynamic semantics را به عنوان static semantics مدل کرده و به احتمال زیاد ما را به سمت ایجاد مشکل Class Proliferation (ازدیاد کلاس ها) سوق خواهد داد.
Abstract Classes
به سوالات زیر توجه کنید:
- آیا هرگز میوه خوردهاید؟
- آیا هرگز پیش غذا خوردهاید؟
- آیا هرگز دسر خوردهاید؟
حال با توجه به سوالات «مزه غذا چطور بود؟ دسری که خوردید، چه تعداد کالری داشت؟ هزینه پیش غذایی که خوردید چقدر بود» پاسخ چه خواهد بود؟
من (نویسنده) ادعا میکنم که هیچ کسی تا به حال میوه نخورده است. بیشتر مردم، سیب، موز و پرتقال خوردهاند؛ میوهی قرمز رنگی به ارزش 3 پوند را نخوردهاند.
شبیه به این مسئله برای زمانی است که گارسون رستوران از شما سوال میکند: «برای شام چه چیزی میل دارید» و شما جواب میدهید: «یک پیش غذا، یک غذای اصلی و یک دسر». در این حالت چون شما دقیقا مشخص نکردهاید چه نوعی میخواهید، گارسون، مات و مبهوت خواهد ماند. همه میدانیم که چیزی تحت عنوان میوه، پیش غذا و یا وهله دسر در واقعیت وجود ندارد؛ بله این عبارات اطلاعات مفیدی را تسخیر میکنند. اگر من در دستم یک ساعت زنگی گرفته و از شما میپرسیدم: «نظرتان در مورد میوه من چیست؟»؛ بدون شک فکر میکردید من دیوانه شدهام. حال اگر در دستم سیبی گرفته و سوال قبلی را میپرسیدم، این بار از نظر شما من یک شخص عاقل بودم.
با وجود اینکه نمیتوان از میوه وهله سازی کرد، اما اطلاعات مفیدی را تسخیر میکند. در واقع میوه، یک کلاسی (concept) است که دانشی از نحوه وهله سازی وهله هایش به وسیله Type پیاده ساز خود، ندارد.
کلاسی که دانشی از نحوه وهله سازی وهلههای خود ندارد، abstract class (کلاس مجرد یا انتزاعی) نامیده میشود.
کلاسی که دانش نحوه وهله سازی وهلههای خود دارد، concrete class نامیده میشود.
در پارادایم شیء گرا، مهمترین استفاده از کلاسهای انتزاعی در مباحث ارث بری مطرح میشود.
Roles Versus Classes
مطمئن باشید انتزاع هایی را که مدل میکنید کلاس بوده و نه نقشهایی که وهلههای آنها بازی میکنند. (Be sure the abstractions that you model are classes and not simply the roles objects play)آیا مادر و پدر به عنوان یک کلاس هستند یا نقشهایی هستند که وهلههای کلاس شخص، بازی میکند؟ پاسخ این سوال وابسته به دامینی (domain) است که طراح در حال مدل سازی آن میباشد. اگر در دامین مورد نظر، مادر و پدر رفتارهای مختلفی دارند، احتمالا باید به عنوان کلاسهای جدا مدل شوند. اگر رفتارهای یکسانی دارند، در نتیجه نقشهای مختلفی هستند که وهلههای کلاس شخص بازی میکنند. به عنوان مثال، میتوان کلاس خانواده را متشکل از وهلهای از کلاس پدر، وهلهای از کلاس مادر و مجموعهای از وهلههای کلاس فرزند در نظر گرفت. در مقابل ممکن است کلاس خانواده را متشکل از وهلهای از کلاس شخص به عنوان پدر، وهلهای از کلاس شخص به عنوان مادر و آرایهای از وهلههای شخص به عنوان فرزندان، مدل کنید. قرار گرفتن در وضیعتی که هر نقش، بخشی از رفتاریهای شخص را مورد استفاده قرار میدهد، کافی نیست و باید مطمئن شوید که رفتارها واقعا متفاوت میباشند. همچنین باید به یاد داشته باشید که زمانیکه وهلهای از بخشی از رفتارهای کلاس خود استفاده میکند، نیز مشکلی وجود ندارد و لازم نیست کلاسهای دیگری را به خاطر این موضوع در طراحی خود در نظر بگیرید.
شکل 2.9 Two views of a family
برخی از طراحان به این شکل تست میکنند که اگر عضوی از واسط عمومی را نمیتوان برای نقش مورد نظر مورد استفاده قرار داد، این موضوع نشان از این دارد که باید برای نقش مورد نظر در طراحی خود کلاس جداگانهای را در نظر داشته باشند. اگر هم عضو مذکور قابل استفاده نباشد، کلاس یکسانی برای نقشهای مختلف استفاده خواهد شد. به عنوان مثال، اگر عملیات ()go_into_labor جزء عملیاتی میباشد که مادر انجام میدهد، در حالیکه پدر چنین عملیاتی را نمیتواند انجام دهد، در این حالت نیز لازم است مادر به عنوان کلاس جداگانهای در نظر گرفته شود. اگر در دامین دیگری، عوض کردن پوشاک را تنها مادر انجام میدهد، در این حالت مادر نقشی از کلاس شخص میباشد، چرا که پدر هم توانایی انجام این عملیات را دارد.
قواعد شهودی فصل دوم
همه دادهها باید در داخل کلاس خود پنهان شده باشند. (All data should be hidden within its class)
استفاده کنندگان از کلاس باید به واسط عمومی آن وابسته باشند، اما یک کلاس نباید به استفاده کنندگان خود، وابسته باشد. (Users of a class must be dependent on its public interface, but a class should not be dependent on its users)
تعداد پیغامهای موجود در قرارداد یک کلاس را کمینه سازید. (Minimize the number of messages in the protocol of a class)
پیاده سازی یک واسط عمومی یکسان کمینه برای همه کلاسها (Implement a minimal public interface that all classes understand [e.g., operations such as copy (deep versus shallow), equality testing, pretty printing, parsing from an ASCII description, etc.].)
جزئیات پیاده سازی، مانند توابع خصوصی common-code ( توابعی که کد مشترک سایر متدهای کلاس را در بدنه خود دارند) را در واسط عمومی یک کلاس قرار ندهید. (Do not put implementation details such as common-code private functions into the public interface of a class)
واسط عمومی کلاس را با اقلامی که یا استفاده کنندگان از کلاس توانایی استفاده از آن را نداشته و یا تمایلی به استفاده از آنها ندارند، آمیخته نکنید. (Do not clutter the public interface of a class with items that users of that class are not able to use or are not interested in using )
اتصال و پیوستگی مابین کلاسها باید از نوع Nil یا Export باشد؛ به این معنی که یک کلاس فقط از واسط عمومی کلاس دیگر استفاده کند یا کاری با آن نداشته باشد. (Classes should only exhibit nil or export coupling with other classes, that is, a class should only use operations in the public interface of another class or have nothing to do with that class.)
یک کلاس باید یک و تنها یک Key Abstraction را تسخیر نماید. (A class should capture one and only one key abstraction)
داده و رفتار مرتبط را در یک جا (کلاس) نگه دارید. (Keep related data and behavior in one place)
اطلاعات نامرتبط به هم را در کلاسهای جدا از هم قرار دهید. ((Spin off nonrelated information into another class (i.e., noncommunicating behavior)
مطمئن باشید انتزاع هایی را که مدل میکنید کلاس بوده و نه نقشهایی که وهلههای آنها بازی میکنند. (Be sure the abstractions that you model are classes and not simply the roles objects play)
- TLS 1.0: The request was aborted: Could not create SSL/TLS secure channel.
- Task List with filter set to Entire Solution doesnt display tasks/todos when the file is closed.
- Fatal error C1001: An internal error has occurred in the compiler.
- VS 2019 Preview 1 - EF6 edmx file cannot be saved.
- vcruntime140.dll should be made available on Microsoft Symbol Server.
- Static Analyser, Custom Rule Set (C++) does not execute included default sets.
- VS2019 Preview: Azure Function publishing does not work.
- References window does not remember its position.
- Missing formatting option for pointers and references.
- Microsoft.TeamFoundation.Client, Version=15.0.0.0 assembly not found when create a new web project.
Command ما که نقش ایجاد یک مشتری را داشت ( CreateCustomerCommand )، هیچ Validation ای برای اعتبار سنجی مقادیر ورودی از سمت کاربر را ندارد و کاربر با هر مقادیری میتواند این Command را فراخوانی کند. در این قسمت با استفاده از کتابخانه Fluent Validation امکان اعتبار سنجی را به Commandهای خود اضافه میکنیم.
در ابتدا با استفاده از دستور زیر ، این کتابخانه را داخل پروژه خود نصب میکنیم:
Install-Package FluentValidation.AspNetCore
بعد از افزودن این کتابخانه، باید آن را داخل DI Container خود Register کنیم:
services.AddMvc() .AddFluentValidation(cfg => cfg.RegisterValidatorsFromAssemblyContaining<Startup>());
کلاس جدیدی با نام CreateCustomerCommandValidator ایجاد میکنیم و از کلاس AbstractValidator مربوط به Fluent Validation ارث بری میکنیم تا منطق اعتبارسنجی برای CreateCustomerCommand را داخل آن تعریف نماییم :
public class CreateCustomerCommandValidator : AbstractValidator<CreateCustomerCommand> { public CreateCustomerCommandValidator() { RuleFor(customer => customer.FirstName).NotEmpty(); RuleFor(customer => customer.LastName).NotEmpty(); } }
اگر برنامه را اجرا و CreateCustomerCommand را با مقادیر خالی فراخوانی کنیم، خواهید دید که بلافاصله با چنین خطایی مواجه خواهید شد که نشان میدهد Fluent Validation بدرستی وظیفه اعتبارسنجی ورودیها را انجام داده است:
Error: Bad Request { "LastName": [ "'Last Name' must not be empty." ], "FirstName": [ "'First Name' must not be empty." ] }
* نکته : تمامی اعتبارسنجیهای سطحی ( Superficial Validation ) مانند خالی نبودن مقادیر، اعتبارسنجی تاریخها، اعتبارسنجی ایمیل و ... باید قبل از Handle شدن Commandها صورت گیرد و در صورت ناموفق بودن اعتبارسنجی، نباید وارد متد Handle در Commandها شویم. ( Fail Fast Principle )
Events
برای حل این مشکل میتوانیم از Eventها استفاده کنیم. Eventها خبری را به Subscriber های خود میدهند. در فریمورک MediatR، ارسال و Handle کردن Eventها، با دو interface صورت میگیرد: INotification و INotificationHandler
بر خلاف Commandها که فقط یک Handler میتوانند داشته باشند، Event ها میتوانند چندین Handler داشته باشند. مزیت داشتن چند Subscriber برای Eventها این است که شما علاوه بر اینکه میتوانید Subscriber ای داشته باشید که وظیفه ارسال Email برای مشتری را بر عهده داشته باشد، Subscriber دیگری داشته باشید که اطلاعات مشتری جدید را Log کند.
ابتدا کلاس CustomerCreatedEvent را ایجاد و از INotification ارث بری میکنیم. این کلاس منتشر کننده یک اتفاق است که آن را Producer مینامند :
public class CustomerCreatedEvent : INotification { public CustomerCreatedEvent(string firstName, string lastName, DateTime registrationDate) { FirstName = firstName; LastName = lastName; RegistrationDate = registrationDate; } public string FirstName { get; } public string LastName { get; } public DateTime RegistrationDate { get; } }
سپس دو Handler برای این Event مینویسیم. Handler اول وظیفه ارسال ایمیل را بر عهده خواهد داشت :
public class CustomerCreatedEmailSenderHandler : INotificationHandler<CustomerCreatedEvent> { public Task Handle(CustomerCreatedEvent notification, CancellationToken cancellationToken) { // IMessageSender.Send($"Welcome {notification.FirstName} {notification.LastName} !"); return Task.CompletedTask; } }
و Handler دوم، وظیفه Log کردن اطلاعات مشتری ثبت شده را بر عهده خواهد داشت:
public class CustomerCreatedLoggerHandler : INotificationHandler<CustomerCreatedEvent> { readonly ILogger<CustomerCreatedLoggerHandler> _logger; public CustomerCreatedLoggerHandler(ILogger<CustomerCreatedLoggerHandler> logger) { _logger = logger; } public Task Handle(CustomerCreatedEvent notification, CancellationToken cancellationToken) { _logger.LogInformation($"New customer has been created at {notification.RegistrationDate}: {notification.FirstName} {notification.LastName}"); return Task.CompletedTask; } }
در نهایت کافیست داخل CreateCustomerCommandHandler که در قسمت قبل آن را ایجاد کردیم، متد Handle را ویرایش و با استفاده از متد Publish موجود در اینترفیس IMediator، این Event را Raise کنیم :
public class CreateCustomerCommandHandler : IRequestHandler<CreateCustomerCommand, CustomerDto> { readonly ApplicationDbContext _context; readonly IMapper _mapper; readonly IMediator _mediator; public CreateCustomerCommandHandler(ApplicationDbContext context, IMapper mapper, IMediator mediator) { _context = context; _mapper = mapper; _mediator = mediator; } public async Task<CustomerDto> Handle(CreateCustomerCommand createCustomerCommand, CancellationToken cancellationToken) { Customer customer = _mapper.Map<Customer>(createCustomerCommand); await _context.Customers.AddAsync(customer, cancellationToken); await _context.SaveChangesAsync(cancellationToken); // Raising Event ... await _mediator.Publish(new CustomerCreatedEvent(customer.FirstName, customer.LastName, customer.RegistrationDate), cancellationToken); return _mapper.Map<CustomerDto>(customer); } }
برنامه را اجرا و روی دو NotificationHandler خود Breakpoint قرار دهید. اگر api/Customers را برای ایجاد یک مشتری جدید فراخوانی کنید، بعد از ثبت نام موفق مشتری، خواهید دید که هر دو Handler شما Raise میشوند و اطلاعات مشتری را که با LogHandler خود داخل Console لاگ کردیم، خواهیم دید:
info: MediatrTutorial.Features.Customer.Events.CustomerCreated.CustomerCreatedLoggerHandler[0] New customer has been created at 2/1/2019 11:40:48 PM: Moien Tajik
* نکته : در این قسمت از آموزش برای Log کردن اطلاعات از یک Notification استفاده کردیم. اگر تعداد Commandهای ما در برنامه بیشتر شوند، به ازای هر Command مجبور به ایجاد یک Notification و NotificationHandler خواهیم بود که منطق کار آنها بسیار شبیه به یک دیگر است.
در مقاله بعدی با استفاده از Behaviors موجود در MediatR که AOP را پیاده سازی میکند، این موارد تکراری را از بین خواهیم برد.
اولین بتای Bootstrap 4 منتشر شد
Two years in the making, we finally have our first beta release of Bootstrap 4. In that time, we’ve broken all the things at least twenty-seven times over with nearly 5,000 commits, 650+ files changed, 67,000 lines added, and 82,000 lines deleted. We also shipped six major alpha releases, a trio of official Themes, and even a job board for good measure.