ASP.NET MVC #24
مروری بر نمونه سؤالات ASP.NET MVC امتحانات مایکروسافت در چند سال اخیر
در قسمت آخر سری ASP.NET MVC بد نیست مروری داشته باشیم بر نمونه سؤالات امتحانات مایکروسافت؛ امتحانات 70-515 و 70-519 که در آنها تعدادی از سؤالات به ASP.NET MVC اختصاص دارند. در این سؤالات امکان انتخاب بیش از یک گزینه نیز وجود دارد.
1) شما در حال توسعه یک برنامهی ASP.NET MVC هستید. باید درخواست Ajax ایی از صفحهای صادر شده و خروجی زیر را از اکشن متدی دریافت کند:
["Adventure Works","Contoso"]
کدام نوع خروجی اکشن متد زیر را برای اینکار مناسب میدانید؟
a) AjaxHelper
b) XDocument
c) JsonResult
d) DataContractJsonSerializer
2) شما در حال طراحی یک برنامه ASP.NET MVC هستید. محتوای یک View باید بر اساس نیازمندیهای زیر تشکیل شود:
الف) ارائه محتوای رندر شده user controls/partial views به مرورگر
ب) کار انتخاب user controls/partial views مناسب در اکشن متد کنترلر باید انجام شود
استفاده از کدام روش زیر را توصیه میکنید؟
a) Use the Html.RenderPartial extension method
b) Use the Html.RenderAction extension method
c) Use the PartialViewResult class
d) Use the ContentResult class
3) در حین طراحی یک برنامه ASP.NET MVC، نیاز است منطق مدیریت استثناهای رخ داده و همچنین ثبت وقایع مرتبط را در یک مکان یا کلاس مرکزی مدیریت کنید. کدام روش زیر را پیشنهاد میدهید؟
a) استفاده از try/catch در تمام متدها
b) تحریف متد OnException در کنترلرها
c) مزین سازی تمام کنترلرها به ویژگی HandleError سفارشی شده
d) مزین سازی تمام کنترلرها به ویژگی HandleError پیش فرض
4) شما در حال توزیع برنامهی ASP.NET MVC خود جهت اجرا بر روی IIS 6.x هستید. چه ملاحظاتی را باید مدنظر داشته باشید تا برنامه به درستی کار کند؟
a) تنظیم IIS به نحویکه تمام درخواستها را بر اساس wildcard خاصی به aspnet_isapi.dll هدایت کند.
b) تنظیم IIS به نحویکه تمام درخواستها را بر اساس wildcard خاصی به aspnet_wp.exe هدایت کند.
c) تغییر برنامه به نحویکه تمام درخواستها را به یک HttpHandler خاص هدایت کند.
d) تغییر برنامه به نحویکه تمام درخواستها را به یک HttpModule خاص هدایت کند.
5) شما در حال توسعه برنامهی ASP.NET MVC هستید که در پوشه Views/Shared/DisplayTemplates آن، فایلی به نام score.cshtml به عنوان یک templated helper نمایش سفارشی اعداد صحیح تعریف شده است. مدل برنامه هم مطابق تعاریف زیر است:
public class Player
{
public String Name { get; set; }
public int LastScore { get; set; }
public int HighScore { get; set; }
}
در اینجا اگر نیاز باشد تا فایل score.cshtml یاد شده به صورت خودکار به خاصیت LastScore در حین فراخوانی متد HtmlHelper.DisplayForModel اعمال شود، چه روشی را پیشنهاد میدهید؟
a) فایل score.cshtml باید به LastScore.cshtml تغییر نام یابد.
b) فایل یاد شده باید از پوشه Views/Shared/DisplayTemplates به پوشه Views/Player/DisplayTemplates منتقل شود.
c) باید از ویژگی UIHint به همراه مقدار score جهت مزین سازی خاصیت LastScore استفاده کرد.
[UIHint("Score")]
[Display(Name="LastScore", ShortName="Score")]
6) شما در حال طراحی برنامهی ASP.NET MVC هستید که در آن متد Edit کنترلری باید تنها توسط کاربران اعتبارسنجی شده قابل دسترسی باشد. استفاده از کدام دو گزینه زیر را برای این منظور توصیه میکنید؟
a) [Authorize(Users = "")]
b) [Authorize(Roles = "")]
c) [Authorize(Users = "*")]
d) [Authorize(Roles = "*")]
7) قطعه کد HTML زیر را درنظر بگیرید:
<span id="ref">
<a name=Reference>Check out</a>
the FAQ on
<a href="http://www.contoso.com">
Contoso</a>'s web site for more information:
<a href="http://www.contoso.com/faq">FAQ</a>.
</span>
<a href="http://www.contoso.com/home">Home</a>
قصد داریم به کمک jQuery در span ایی با id مساوی ref، متن تمام لینکها را ضخیم کنیم. کدام گزینه زیر را پیشنهاد میدهید؟
a) $("#ref").filter("a[href]").bold();
b) $("ref").filter("a").css("bold");
c) $("a").css({fontWeight:"bold"});
d) $("#ref a[href]").css({fontWeight:"bold"});
ایجاد ماژول Dashboard و تعریف کامپوننت صفحهی محافظت شده
قصد داریم پس از لاگین موفق، کاربر را به یک صفحهی محافظت شده هدایت کنیم. به همین جهت ماژول جدید Dashboard را به همراه کامپوننت یاد شده، به برنامه اضافه میکنیم:
>ng g m Dashboard -m app.module --routing >ng g c Dashboard/ProtectedPage
import { DashboardModule } from "./dashboard/dashboard.module"; @NgModule({ imports: [ //... DashboardModule, AppRoutingModule ] }) export class AppModule { }
import { ProtectedPageComponent } from "./protected-page/protected-page.component"; const routes: Routes = [ { path: "protectedPage", component: ProtectedPageComponent } ];
ایجاد صفحهی ورود به سیستم
در قسمت اول این سری، کارهای «ایجاد ماژول Authentication و تعریف کامپوننت لاگین» انجام شدند. اکنون میخواهیم کامپوننت خالی لاگین را به نحو ذیل تکمیل کنیم:
export class LoginComponent implements OnInit { model: Credentials = { username: "", password: "", rememberMe: false }; error = ""; returnUrl: string;
constructor( private authService: AuthService, private router: Router, private route: ActivatedRoute) { } ngOnInit() { // reset the login status this.authService.logout(false); // get the return url from route parameters this.returnUrl = this.route.snapshot.queryParams["returnUrl"]; }
اکنون متد ارسال این فرم چنین شکلی را پیدا میکند:
submitForm(form: NgForm) { this.error = ""; this.authService.login(this.model) .subscribe(isLoggedIn => { if (isLoggedIn) { if (this.returnUrl) { this.router.navigate([this.returnUrl]); } else { this.router.navigate(["/protectedPage"]); } } }, (error: HttpErrorResponse) => { console.log("Login error", error); if (error.status === 401) { this.error = "Invalid User name or Password. Please try again."; } else { this.error = `${error.statusText}: ${error.message}`; } }); }
صفحهی خالی protectedPage را در ابتدای بحث، در ذیل ماژول Dashboard ایجاد کردیم.
در سمت سرور هم در صورت شکست اعتبارسنجی کاربر، یک return Unauthorized صورت میگیرد که معادل error.status === 401 کدهای فوق است و در اینجا در قسمت خطای عملیات بررسی شدهاست.
قالب این کامپوننت نیز به صورت ذیل به model از نوع Credentials آن متصل شدهاست:
<div class="panel panel-default"> <div class="panel-heading"> <h2 class="panel-title">Login</h2> </div> <div class="panel-body"> <form #form="ngForm" (submit)="submitForm(form)" novalidate> <div class="form-group" [class.has-error]="username.invalid && username.touched"> <label for="username">User name</label> <input id="username" type="text" required name="username" [(ngModel)]="model.username" #username="ngModel" class="form-control" placeholder="User name"> <div *ngIf="username.invalid && username.touched"> <div class="alert alert-danger" *ngIf="username.errors['required']"> Name is required. </div> </div> </div> <div class="form-group" [class.has-error]="password.invalid && password.touched"> <label for="password">Password</label> <input id="password" type="password" required name="password" [(ngModel)]="model.password" #password="ngModel" class="form-control" placeholder="Password"> <div *ngIf="password.invalid && password.touched"> <div class="alert alert-danger" *ngIf="password.errors['required']"> Password is required. </div> </div> </div> <div class="checkbox"> <label> <input type="checkbox" name="rememberMe" [(ngModel)]="model.rememberMe"> Remember me </label> </div> <div class="form-group"> <button type="submit" class="btn btn-primary" [disabled]="form.invalid ">Login</button> </div> <div *ngIf="error" class="alert alert-danger " role="alert "> {{error}} </div> </form> </div> </div>
برای آزمایش برنامه، نام کاربری Vahid و کلمهی عبور 1234 را وارد کنید.
تکمیل کامپوننت Header برنامه
در ادامه، پس از لاگین موفق شخص، میخواهیم صفحهی protectedPage را نمایش دهیم:
در این صفحه، Login از منوی سایت حذف شدهاست و بجای آن Logout به همراه «نام نمایشی کاربر» ظاهر شدهاند. همچنین توکن decode شده به همراه تاریخ انقضای آن نمایش داده شدهاند.
برای پیاده سازی این موارد، ابتدا از کامپوننت Header شروع میکنیم:
export class HeaderComponent implements OnInit, OnDestroy { title = "Angular.Jwt.Core"; isLoggedIn: boolean; subscription: Subscription; displayName: string; constructor(private authService: AuthService) { }
اکنون در روال رخدادگردان ngOnInit، مشترک authStatus میشود که یک BehaviorSubject است و از آن جهت صدور رخدادهای authService به تمام کامپوننتهای مشترک به آن استفاده کردهایم:
ngOnInit() { this.subscription = this.authService.authStatus$.subscribe(status => { this.isLoggedIn = status; if (status) { this.displayName = this.authService.getDisplayName(); } }); }
همچنین اگر این وضعیت true باشد، مقدار DisplayName کاربر را نیز از سرویس authService دریافت کرده و توسط خاصیت this.displayName در اختیار قالب Header قرار میدهیم.
در آخر برای جلوگیری از نشتی حافظه، ضروری است اشتراک به authStatus، در روال رخدادگردان ngOnDestroy لغو شود:
ngOnDestroy() { // prevent memory leak when component is destroyed this.subscription.unsubscribe(); }
همچنین در قالب Header، مدیریت دکمهی Logout را نیز انجام خواهیم داد:
logout() { this.authService.logout(true); }
با این مقدمات، قالب Header اکنون به صورت ذیل تغییر میکند:
<nav class="navbar navbar-default"> <div class="container-fluid"> <div class="navbar-header"> <a class="navbar-brand" [routerLink]="['/']">{{title}}</a> </div> <ul class="nav navbar-nav"> <li class="nav-item" routerLinkActive="active" [routerLinkActiveOptions]="{ exact: true }"> <a class="nav-link" [routerLink]="['/welcome']">Home</a> </li> <li *ngIf="!isLoggedIn" class="nav-item" routerLinkActive="active"> <a class="nav-link" queryParamsHandling="merge" [routerLink]="['/login']">Login</a> </li> <li *ngIf="isLoggedIn" class="nav-item" routerLinkActive="active"> <a class="nav-link" (click)="logout()">Logoff [{{displayName}}]</a> </li> <li *ngIf="isLoggedIn" class="nav-item" routerLinkActive="active"> <a class="nav-link" [routerLink]="['/protectedPage']">Protected Page</a> </li> </ul> </div> </nav>
تکمیل کامپوننت صفحهی محافظت شده
در تصویر قبل، نمایش توکن decode شده را نیز مشاهده کردید. این نمایش توسط کامپوننت صفحهی محافظت شده، مدیریت میشود:
import { Component, OnInit } from "@angular/core"; import { AuthService } from "../../core/services/auth.service"; @Component({ selector: "app-protected-page", templateUrl: "./protected-page.component.html", styleUrls: ["./protected-page.component.css"] }) export class ProtectedPageComponent implements OnInit { decodedAccessToken: any = {}; accessTokenExpirationDate: Date = null; constructor(private authService: AuthService) { } ngOnInit() { this.decodedAccessToken = this.authService.getDecodedAccessToken(); this.accessTokenExpirationDate = this.authService.getAccessTokenExpirationDate(); } }
<h1> Decoded Access Token </h1> <div class="alert alert-info"> <label> Access Token Expiration Date:</label> {{accessTokenExpirationDate}} </div> <div> <pre>{{decodedAccessToken | json}}</pre> </div>
کدهای کامل این سری را از اینجا میتوانید دریافت کنید.
برای اجرای آن فرض بر این است که پیشتر Angular CLI را نصب کردهاید. سپس از طریق خط فرمان به ریشهی پروژهی ASPNETCore2JwtAuthentication.AngularClient وارد شده و دستور npm install را صادر کنید تا وابستگیهای آن دریافت و نصب شوند. در آخر با اجرای دستور ng serve -o، برنامه ساخته شده و در مرورگر پیش فرض سیستم نمایش داده خواهد شد (و یا همان اجرای فایل ng-serve.bat). همچنین باید به پوشهی ASPNETCore2JwtAuthentication.WebApp نیز مراجعه کرده و فایل dotnet_run.bat را اجرا کنید، تا توکن سرور برنامه نیز فعال شود.
public class Mailer { public static bool SendEmail() { Console.WriteLine("Sending Mail ..."); // simulate error Random rnd = new Random(); var rndNumber = rnd.Next(1, 10); if (rndNumber != 3) // * throw new SmtpFailedRecipientException(); Console.WriteLine("Mail Sent successfully"); return true; } }
public static class Retry { public static void Do(Action action,TimeSpan retryInterval,int maxAttemptCount = 3) { Do<object>(() => { action(); return null; }, retryInterval, maxAttemptCount); } public static T Do<T>(Func<T> action,TimeSpan retryInterval,int maxAttemptCount = 3) { var exceptions = new List<Exception>(); for (int attempted = 0; attempted < maxAttemptCount; attempted++) { try { if (attempted > 0) { Thread.Sleep(retryInterval); } return action(); } catch (Exception ex) { exceptions.Add(ex); } } throw new AggregateException(exceptions); } }
retryInterval= retryInterval.Add(TimeSpan.FromSeconds(10));
در سادهترین حالت، استفاده از Polly همانند زیر است:
var policy = Policy.Handle<SmtpFailedRecipientException>().Retry(); policy.Execute(Mailer.SendEmail);
متد Retry، دارای Overloadهای مختلفی است که یکی از آنها مقدار تعداد دفعات تلاش را دریافت میکند؛ همانند:
var policy = Policy.Handle<SmtpFailedRecipientException>().Retry(5);
لازم به ذکر است که باید دقیقا Exception مورد نظر را در بخش Config به کار ببرید. برای نمونه اگر کد فوق را همانند زیر به کار ببرید، در صورتیکه متد ارسال ایمیل با خطایی مواجه شود، هیچ تلاشی برای اجرای مجدد نخواهد کرد:
var policy = Policy.Handle<SqlException>().Retry(5);
برای نمونه میتوان از متد ForEver آن استفاده کرد تا زمانیکه متد مورد نظر Success نشده باشد، سعی در اجرای آن کند:
Policy .Handle<DivideByZeroException>() .RetryForever()
اگر به جستجوی گوگل دقت کرده باشید، به صورت Ajax ایی پیاده سازی شدهاست، با این تفاوت که بعد از هر تغییر درجستجوی مورد نظر، Url صفحه نیز تغییر میکند (برای مثال بعد از جستجوی عبارت dotNetTips آدرس بار صفحه به شکل https://www.google.com/#q=dotNetTips&* تغییر میکند). برای پیاده سازی این ویژگی باید از تکنیکی به نام HashChange استفاده کرد. در نتیجه با این روش مشکل ارسال صفحهای خاص در یک گرید برای دیگران، به صورت Ajax ایی و بدون مشکل انجام میشود. از این رو با توجه به داشتن Urlهای منحصر به فرد برای هر صفحه، تا حدی مشکل سئو سایت را نیز برطرف میکنیم.
برای استفاده از این ویژگی در ادامه قصد داریم پیاده سازی کتابخانهی MvcAjaxPager را مورد بررسی قرار دهیم. ابتدا قبل از هر کاری، با استفاده از دستور زیر اقدام به نصب کتابخانه آن مینماییم:
Install-Package MvcAjaxPager
در ادامه نحوه پیاده سازی آن را به همراه مثالی، مورد بررسی قرار میدهیم:
ابتدا یک مدل فرضی را همانند زیر تهیه میکنیم :
public class Topic { public int Id; public string Title; public string Text; }
public class TopicService { public static IEnumerable<Topic> Topics = new List<Topic>() { new Topic{Id=1,Title="Title 1",Text= "Text 1"}, new Topic{Id=2,Title="Title 2",Text="Text 2"}, new Topic{Id=3,Title="Title 3",Text="Text 3"}, new Topic{Id=4,Title="Title 4",Text="Text 4"}, new Topic{Id=5,Title="Title 5",Text="Text 5"}, new Topic{Id=6,Title="Title 6",Text="Text 6"}, new Topic{Id=7,Title="Title 7",Text="Text 7"}, new Topic{Id=8,Title="Title 8",Text="Text 8"}, new Topic{Id=9,Title="Title 9",Text="Text 9"}, new Topic{Id=10,Title="Title 10",Text="Text 10"}, new Topic{Id=11,Title="Title 11",Text="Text 11"}, new Topic{Id=12,Title="Title 12",Text="Text 12"}, new Topic{Id=13,Title="Title 13",Text="Text 13"}, new Topic{Id=14,Title="Title 14",Text="Text 14"}, new Topic{Id=15,Title="Title 15",Text="Text 15"}, new Topic{Id=16,Title="Title 16",Text="Text 16"}, new Topic{Id=17,Title="Title 17",Text="Text 17"}, new Topic{Id=18,Title="Title 18",Text="Text 18"}, new Topic{Id=19,Title="Title 19",Text="Text 19"}, new Topic{Id=20,Title="Title 20",Text="Text 20"}, new Topic{Id=21,Title="Title 21",Text="Text 21"}, new Topic{Id=22,Title="Title 22",Text="Text 22"}, }; public static IEnumerable<Topic> GetAll() { return Topics.OrderBy(row => row.Id); } }
public class ListViewModel { public IEnumerable<Topic> Topics { get; set; } public int PageIndex { get; set; } public int TotalItemCount { get; set; } }
public ActionResult Index(int page = 1) { var topics = TopicService.GetAll (); int totalItemCount = topics.Count(); var model = new ListViewModel() { PageIndex = page, Topics = topics.OrderBy(p => p.Id).Skip((page - 1) * 10).Take(10).ToList(), TotalItemCount = totalItemCount }; if (!Request.IsAjaxRequest()) { return View(model); } return PartialView("_TopicList", model); }
و در Partial view مربوطه نیز داریم :
@using MvcAjaxPager @model ListViewModel @Html.AjaxPager(Model.TotalItemCount, 10, Model.PageIndex, "Index", "Home", null, new PagerOptions { ShowDisabledPagerItems = true, AlwaysShowFirstLastPageNumber = true, HorizontalAlign = "center", ShowFirstLast = false, CssClass = "NavigationBox", AjaxUpdateTargetId = "dvTopics", AjaxOnBegin = "AjaxStart", AjaxOnComplete = "AjaxStop" }, null, null) <table> <tr> <th> @Html.DisplayName("ID") </th> <th> @Html.DisplayName("Title") </th> <th> @Html.DisplayName("Text") </th> </tr> @foreach (var topic in Model.Topics) { <tr> <td> @topic.Id </td> <td> @topic.Title </td> <td> @topic.Text </td> </tr> } </table> @Html.AjaxPager(Model.TotalItemCount, 10, Model.PageIndex, "Index", "Home", null, new PagerOptions { ShowDisabledPagerItems = true, AlwaysShowFirstLastPageNumber = true, HorizontalAlign = "center", ShowFirstLast = true, FirstPageText = "اولین", LastPageText = "آخرین", MorePageText = "...", PrevPageText = "قبلی", NextPageText = "بعدی", CssClass = "NavigationBox", AjaxUpdateTargetId = "dvTopics", AjaxOnBegin = "AjaxStart", AjaxOnComplete = "AjaxStop" }, null, null)
حال برای استفاده از pager مورد نظر فقط کافیست متد AjaxPager آن را فراخوانی کنیم. این متد شامل 11 OverLoad مختلف هست.
در این قسمت TotalItemCount جمع کل رکوردها، PageSize تعداد رکوردهای هر صفحه و PageIndex آدرس صفحه جاری میباشد.
مهمترین بخش این pager که قابلیتهای زیادی را به کاربر میدهد، قسمت PagerOptions آن است و تعدادی از پارامترهای آن شامل AjaxOnBeginAjaxOnCompelte، AjaxOnSuccess ، AjaxOnFailure میتوان تعیین کرد تا بعد از شروع، وقوع خطا، موفقیت و یا خاتمه عملیات جاوا اسکریپتی، اجرا شود.
AlwaysShowFirstLastPageNumber جهت نمایش صفحه اول و آخر
FirstPageText جهت تعیین متن اولین صفحه
LastPageText جهت تعیین متن آخرین صفحه
CssClass ، Id جهت تعیین Id خاص
و در انتها، در view مربوطه داریم:
@using MvcAjaxPager @model ListViewModel @{ Layout = null; } <!DOCTYPE html> <html> <head> <meta name="viewport" content="width=device-width" /> <title>Index</title> </head> <body> <div id="dvTopics"> @{ @Html.Partial("_TopicList", Model); } </div> <script type="text/javascript" src="@Url.Content("~/Scripts/jquery-1.7.2.min.js")"></script> <script type="text/javascript" src="@Url.Content("~/Scripts/path.min.js")"></script> <script type="text/javascript" src="@Url.Content("~/Scripts/jquery.pager-1.0.1.min.js")"></script> <script type="text/javascript"> $('.NavigationBox').pager(); //pagination before start function AjaxStart() { console.log('Start AJAX call. Loading message can be shown'); } // pagination - after request function AjaxStop() { console.log('Stop AJAX call. Loading message can be hidden'); }; </script> </body> </html>
شی گرایی در #F
*با توجه به این موضوع که فرض است دوستان با مفاهیم شی گرایی آشنایی دارند از توضیح و تشریح این مفاهیم خودداری میکنم.
Classes
کلاس چارچوبی از اشیا است برای نگهداری خواص(Properties) و رفتار ها(Methods) و رخدادها(Events). کلاس پایه ایترین مفهوم در برنامه نویسی شی گراست. ساختار کلی تعربف کلاس در #F به صورت زیر است:
type [access-modifier] type-name [type-params] [access-modifier] ( parameter-list ) [ as identifier ] = [ class ] [ inherit base-type-name(base-constructor-args) ] [ let-bindings ] [ do-bindings ] member-list ... [ end ] type [access-modifier] type-name1 ... and [access-modifier] type-name2 ... ...
انواع access-modifier در #F
- public : دسترسی برای تمام فراخوانها امکان پذیر است
- internal : دسترسی برای تمام فراخوان هایی که در همین assembly هستند امکان پذیر است
- private : دسترسی فقط برای فراخوانهای موجود در همین ماژول امکان پذیر است
نکته : protected access modifier در #F پشتیبانی نمیشود.
مثالی از تعریف کلاس:
type Account(number : int, name : string) = class let mutable amount = 0m end
let myAccount = new Account(123456, "Masoud")
برای تعریف خاصیت در #F باید از کلمه کلیدی member استفاده کنید. در مثال بعدی برای کلاس بالا تابع و خاصیت تعریف خواهیم کرد.
type Account(number : int, name: string) = class let mutable amount = 0m member x.Number = number member x.Name= name member x.Amount = amount member x.Deposit(value) = amount <- amount + value member x.Withdraw(value) = amount <- amount - value end
open System type Account(number : int, name: string) = class let mutable amount = 0m member x.Number = number member x.Name= name member x.Amount = amount member x.Deposit(value) = amount <- amount + value member x.Withdraw(value) = amount <- amount - value end
let masoud= new Account(12345, "Masoud") let saeed = new Account(67890, "Saeed") let transfer amount (source : Account) (target : Account) = source.Withdraw amount target.Deposit amount let printAccount (x : Account) = printfn "x.Number: %i, x.Name: %s, x.Amount: %M" x.Number x.Name x.Amount let main() = let printAccounts() = [masoud; saeed] |> Seq.iter printAccount printfn "\nInializing account" homer.Deposit 50M marge.Deposit 100M printAccounts() printfn "\nTransferring $30 from Masoud to Saeed" transfer 30M masoud saeed
printAccounts() printfn "\nTransferring $75 from Saeed to Masoud" transfer 75M saeed masoud printAccounts() main()
در #F زمانی که قصد داشته باشیم در بعد از وهله سازی از کلاس و فراخوانی سازنده، عملیات خاصی انجام شود(مثل انجام برخی عملیات متداول در سازندههای کلاسهای دات نت) باید از کلمه کلیدی do به همراه یک بلاک از کد استفاده کنیم.
open System open System.Net type Stock(symbol : string) = class let mutable _symbol = String.Empty do //کد مورد نظر در این جا نوشته میشود end
open System type MyType(a:int, b:int) as this = inherit Object() let x = 2*a let y = 2*b do printfn "Initializing object %d %d %d %d %d %d" a b x y (this.Prop1) (this.Prop2) static do printfn "Initializing MyType." member this.Prop1 = 4*x member this.Prop2 = 4*y override this.ToString() = System.String.Format("{0} {1}", this.Prop1, this.Prop2) let obj1 = new MyType(1, 2)
خروجی مثال بالا:
Initializing MyType. Initializing object 1 2 2 4 8 16
برای تعریف خواص به صورت استاتیک مانند #C از کلمه کلیدی static استفاده کنید.مثالی در این زمینه:
type SomeClass(prop : int) = class member x.Prop = prop static member SomeStaticMethod = "This is a static method" end
let instance = new SomeClass(5);; instance.SomeStaticMethod;; output: stdin(81,1): error FS0191: property 'SomeStaticMethod' is static.
SomeClass.SomeStaticMethod;; (* invoking static method *)
همانند #C و سایر زبانهای دات نت امکان تعریف متدهای get و set برای خاصیتهای یک کلاس وجود دارد.
ساختار کلی:
member alias.PropertyName with get() = some-value and set(value) = some-assignment
type MyClass() = class let mutable num = 0 member x.Num with get() = num and set(value) = num <- value end;;
public int Num { get{return num;} set{num=value;} }
type MyClass() = class let mutable num = 0 member x.Num with get() = num and set(value) = if value > 10 || value < 0 then raise (new Exception("Values must be between 0 and 10")) else num <- value end
Interface ها
اینترفیس به تمامی خواص و توابع عمومی اشئایی که آن را پیاده سازی کرده اند اشاره میکند. (توضیحات بیشتر (^ ) و (^ ))ساختار کلی برای تعریف آن به صورت زیر است:
type type-name = interface inherits-decl member-defns end
type IPrintable = abstract member Print : unit -> unit
نکته: در هنگام تعریف توابع و خاصیت در interfaceها باید از کلمه abstract استفاده کنیم. هر کلاسی که از یک یا چند تا اینترفیس ارث ببرد باید تمام خواص و توابع اینتریسها را پیاده سازی کند. در مثال بعدی کلاس SomeClass1 اینترفیس بالا را پیاده سازی میکند. دقت کنید که کلمه this توسط من به عنوان اشاره گر به اشیای کلاس تعیین شده و شما میتونید از هر کلمه یا حرف دیگری استفاده کنید.
type SomeClass1(x: int, y: float) = interface IPrintable with member this.Print() = printfn "%d %f" x y
روش نادرست:
let instance = new SomeClass1(10,20) instance.Print//فراخوانی این متد باعث ایجاد خطای کامپایلری میشود.
let instance = new SomeClass1(10,20) let instanceCast = instance :> IPrintable// استفاده از (<:) برای عملیات تبدیل کلاس به اینترفیس instanceCast.Print
در مثال بعدی کلاسی خواهیم داشت که از سه اینترفیس ارث میبرد. در نتیجه باید تمام متدهای هر سه اینترفیس را پیاده سازی کند.
type Interface1 = abstract member Method1 : int -> int type Interface2 = abstract member Method2 : int -> int type Interface3 = inherit Interface1 inherit Interface2 abstract member Method3 : int -> int type MyClass() = interface Interface3 with member this.Method1(n) = 2 * n member this.Method2(n) = n + 100 member this.Method3(n) = n / 10
let instance = new MyClass() let instanceToCast = instance :> Interface3 instanceToCast.Method3 10
#F از کلاسهای abstract هم پشتیبانی میکند. اگر با کلاسهای abstract در #C آشنایی ندارید میتونید مطالب مورد نظر رو در (^ ) و (^ ) مطالعه کنید. به صورت خلاصه کلاسهای abstract به عنوان کلاسهای پایه در برنامه نویسی شی گرا استفاده میشوند. این کلاسها دارای خواص و متدهای پیاده سازی شده و نشده هستند. خواص و متد هایی که در کلاس پایه abstract پیاده سازی نشده اند باید توسط کلاس هایی که از این کلاس پایه ارث میبرند حتما پیاده سازی شوند.
ساختار کلی تعریف کلاسهای abstract:
[<AbstractClass>] type [ accessibility-modifier ] abstract-class-name = [ inherit base-class-or-interface-name ] [ abstract-member-declarations-and-member-definitions ] abstract member member-name : type-signature
[<AbstractClass>] type Shape(x0 : float, y0 : float) = let mutable x, y = x0, y0 let mutable rotAngle = 0.0 abstract Area : float with get abstract Perimeter : float with get abstract Name : string with get
#1 کلاس اول
type Square(x, y,SideLength) = inherit Shape(x, y)
override this.Area = this.SideLength * this.SideLength override this.Perimeter = this.SideLength * 4. override this.Name = "Square"
type Circle(x, y, radius) = inherit Shape(x, y)
let PI = 3.141592654 member this.Radius = radius override this.Area = PI * this.Radius * this.Radius override this.Perimeter = 2. * PI * this.Radius
structureها در #F دقیقا معال struct در #C هستند. توضیحات بیشتر درباره struct در #C (^ ) و (^ )). اما به طور خلاصه باید ذکر کنم که strucureها تقریبا دارای مفهوم کلاس هستند با اندکی تفاوت که شامل موارد زیر است:
- structureها از نوع مقداری هستند و این بدین معنی است مستقیما درون پشته ذخیره میشوند.
- ارجاع به structureها از نوع ارجاع با مقدار است بر خلاف کلاسها که از نوع ارجاع به منبع هستند.(^ )
- structureها دارای خواص ارث بری نیستند.
- عموما از structure برای ذخیره مجموعه ای از دادهها با حجم و اندازه کم استفاده میشود.
ساختار کلی تعریف structure
[ attributes ] type [accessibility-modifier] type-name = struct type-definition-elements end //یا به صورت زیر [ attributes ] [<StructAttribute>] type [accessibility-modifier] type-name = type-definition-elements
type Point3D = struct val x: float val y: float val z: float end
type Point2D = struct val X: float val Y: float new(x: float, y: float) = { X = x; Y = y } end
در پایان یک مثال مشترک رو در #C و #F پیاده سازی میکنیم:
شروع به کار با RavenDB
- مروری بر مفاهیم مقدماتی NoSQL
- ردهها و انواع مختلف بانکهای اطلاعاتی NoSQL
- چه زمانی بهتر است از بانکهای اطلاعاتی NoSQL استفاده کرد و چه زمانی خیر؟
لطفا یکبار این پیشنیازها را پیش از شروع به کار مطالعه نمائید؛ چون بسیاری از مفاهیم پایهای و اصطلاحات مرسوم دنیای NoSQL در این سه قسمت بررسی شدهاند و از تکرار مجدد آنها در اینجا صرفنظر خواهد شد.
RavenDB چیست؟
RavenDB یک بانک اطلاعاتی سورس باز NoSQL سندگرای تهیه شده با دات نت است. ساختار کلی بانکهای اطلاعاتی NoSQL سندگرا، از لحاظ نحوه ذخیره سازی اطلاعات، با بانکهای اطلاعاتی رابطهای متداول، کاملا متفاوت است. در اینگونه بانکهای اطلاعاتی، رکوردهای اطلاعات، به صورت اشیاء JSON ذخیره میشوند. اشیاء JSON یا JavaScript Object Notation بسیار شبیه به anonymous objects سی شارپ هستند. JSON روشی است که توسط آن JavaScript اشیاء خود را معرفی و ذخیره میکند. به عنوان رقیبی برای XML مطرح است؛ نسبت به XML اندکی فشردهتر بوده و عموما دارای اسکیمای خاصی نیست و در بسیاری از اوقات تفسیر المانهای آن به مصرف کننده واگذار میشود.
در JSON عموما سه نوع المان پایه مشاهده میشوند:
- اشیاء که به صورت {object} تعریف میشوند.
- مقادیر "key":"value" که شبیه به نام خواص و مقادیر آنها در دات نت هستند.
- و آرایهها به صورت [array]
همچنین ترکیبی از این سه عنصر یاد شده نیز همواره میسر است. برای مثال، یک key مشخص میتواند دارای مقداری حاوی یک آرایه یا شیء نیز باشد.
JSON: JavaScript Object Notation document :{ key: "Value", another_key: { name: "embedded object" }, some_date: new Date(), some_number: 12 } C# anonymous object var Document = new { Key= "Value", AnotherKey= new { Name = "embedded object" }, SomeDate = DateTime.Now(), SomeNumber = 12 };
یک نکته مهم
اگر پیشنیازهای بحث را مطالعه کرده باشید، حتما بارها با این جمله که دنیای NoSQL از تراکنشها پشتیبانی نمیکند، برخورد داشتهاید. این مطلب در مورد RavenDB صادق نیست و این بانک اطلاعاتی NoSQL خاص، از تراکنشها پشتیبانی میکند. RavenDB در Document store خود ACID عملکرده و از تراکنشها پشتیبانی میکند. اما تهیه ایندکسهای آن بر مبنای مفهوم عاقبت یک دست شدن عمل میکند.
مجوز استفاده از RavenDB
هرچند مجموعه سرور و کلاینت RavenDB سورس باز هستند، اما این مورد به معنای رایگان بودن آن نیست. مجوز استفاده از RavenDB نوع خاصی به نام AGPL است. به این معنا که یا کل کار مشتق شده خود را باید به صورت رایگان و سورس باز ارائه دهید و یا اینکه مجوز استفاده از آنرا برای کارهای تجاری بسته خود خریداری نمائید. نسخه استاندارد آن نزدیک به هزار دلار است و نسخه سازمانی آن نزدیک به 2800 دلار به ازای هر سرور.
شروع به نوشتن اولین برنامه کار با RavenDB
ابتدا یک پروژه کنسول ساده را آغاز کنید. سپس کلاسهای مدل زیر را به آن اضافه نمائید:
using System.Collections.Generic; namespace RavenDBSample01.Models { public class Question { public string By { set; get; } public string Title { set; get; } public string Content { set; get; } public List<Comment> Comments { set; get; } public List<Answer> Answers { set; get; } public Question() { Comments = new List<Comment>(); Answers = new List<Answer>(); } } } namespace RavenDBSample01.Models { public class Comment { public string By { set; get; } public string Content { set; get; } } } namespace RavenDBSample01.Models { public class Answer { public string By { set; get; } public string Content { set; get; } } }
PM> Install-Package RavenDB.Client PM> Install-Package RavenDB.Server
اکنون به پوشه packages\RavenDB.Server.2.5.2700\tools مراجعه کرده و برنامه Raven.Server.exe را اجرا کنید تا سرور RavenDB شروع به کار کند. این سرور به صورت پیش فرض بر روی پورت 8080 اجرا میشود. از این جهت که در RavenDB نیز همانند سایر Document Stores مطرح، امکان دسترسی به اسناد از طریق REST API و Urlها وجود دارد.
البته لازم به ذکر است که RavenDB در 4 حالت برنامه کنسول (همین سرور فوق)، نصب به عنوان یک سرویس ویندوز NT، هاست شدن در IIS و حالت مدفون شده یا Embedded قابل استفاده است.
خوب؛ همین اندازه برای برپایی اولیه RavenDB کفایت میکند.
using Raven.Client.Document; using RavenDBSample01.Models; namespace RavenDBSample01 { class Program { static void Main(string[] args) { using (var store = new DocumentStore { Url = "http://localhost:8080" }.Initialize()) { using (var session = store.OpenSession()) { session.Store(new Question { By = "users/Vahid", Title = "Raven Intro", Content = "Test...." }); session.SaveChanges(); } } } } }
کار با ایجاد یک DocumentStore که به آدرس سرور اشاره میکند و کار مدیریت اتصالات را برعهده دارد، شروع خواهد شد. اگر نمیخواهید Url را درون کدهای برنامه مقدار دهی کنید، میتوان از فایل کانفیگ برنامه نیز برای این منظور کمک گرفت:
<connectionStrings> <add name="ravenDB" connectionString="Url=http://localhost:8080"/> </connectionStrings>
سپس با ایجاد Session در حقیقت یک Unit of work آغاز میشود که درون آن میتوان انواع و اقسام دستورات را صادر نمود و سپس در پایان کار، با فراخوانی SaveChanges، این اعمال ذخیره میگردند. در RavenDB یک سشن باید طول عمری کوتاه داشته باشد و اگر تعداد عملیاتی که در آن صادر کردهاید، زیاد است با خطای زیر متوقف خواهید شد:
The maximum number of requests (30) allowed for this session has been reached.
در یک برنامه واقعی، ایجاد DocumentStore یکبار در آغاز کار برنامه باید انجام گردد. اما هر سشن یا هر واحد کاری آن، به ازای تراکنشهای مختلفی که باید صورت گیرند، بر روی این DocumentStore، ایجاد شده و سپس بسته خواهند شد. برای مثال در یک برنامه ASP.NET، در فایل Global.asax در زمان آغاز برنامه، کار ایجاد DocumentStore انجام شده و سپس به ازای هر درخواست رسیده، یک سشن RavenDB ایجاد و در پایان درخواست، این سشن آزاد خواهد شد.
برنامه را اجرا کنید، سپس به کنسول سرور RavenDB که پیشتر آنرا اجرا نمودیم مراجعه نمائید تا نمایی از عملیات انجام شده را بتوان مشاهده کرد:
Raven is ready to process requests. Build 2700, Version 2.5.0 / 6dce79a Server started in 14,438 ms Data directory: D:\Prog\RavenDBSample01\packages\RavenDB.Server.2.5.2700\tools\Database\System HostName: <any> Port: 8080, Storage: Esent Server Url: http://localhost:8080/ Available commands: cls, reset, gc, q Request # 1: GET - 514 ms - <system> - 404 - /docs/Raven/Replication/Destinations Request # 2: GET - 763 ms - <system> - 200 - /queries/?&id=Raven%2FHilo%2Fquestions&id=Raven%2FServerPrefixForHilo Request # 3: PUT - 185 ms - <system> - 201 - /docs/Raven/Hilo/questions Request # 4: POST - 103 ms - <system> - 200 - /bulk_docs PUT questions/1
عملیات PUT حتما نیاز به یک Id از پیش مشخص دارد و این Id، پیشتر در سطری که Hilo در آن ذکر شده (یکی از الگوریتمهای محاسبه Id در RavenDB)، محاسبه گردیده است. برای نمونه در اینجا الگوریتم Hilo مقدار "questions/1" را به عنوان Id محاسبه شده بازگشت داده است.
در سطری که عملیات Post به آدرس bulk_docs سرور ارسال گردیده است، کار ارسال یکباره چندین شیء JSON برای کاهش رفت و برگشتها به سرور انجام میشود.
و برای کوئری گرفتن مقدماتی از اطلاعات ثبت شده میتوان نوشت:
using (var session = store.OpenSession()) { var question1 = session.Load<Question>("questions/1"); Console.WriteLine(question1.By); }
نگاهی به بانک اطلاعاتی ایجاد شده
در همین حال که سرور RavenDB در حال اجرا است، مرورگر دلخواه خود را گشوده و سپس آدرس http://localhost:8080 را وارد نمائید. بلافاصله، کنسول مدیریتی تحت وب این بانک اطلاعاتی که با سیلورلایت نوشته شده است، ظاهر خواهد شد:
و اگر بر روی هر سطر اطلاعات دوبار کلیک کنید، به معادل JSON آن نیز خواهید رسید:
اینبار برنامه را به صورت زیر تغییر دهید تا روابط بین کلاسها را نیز پیاده سازی کند:
using System; using Raven.Client.Document; using RavenDBSample01.Models; namespace RavenDBSample01 { class Program { static void Main(string[] args) { using (var store = new DocumentStore { Url = "http://localhost:8080" }.Initialize()) { using (var session = store.OpenSession()) { var question = new Question { By = "users/Vahid", Title = "Raven Intro", Content = "Test...." }; question.Answers.Add(new Answer { By = "users/Farid", Content = "بررسی میشود" }); session.Store(question); session.SaveChanges(); } using (var session = store.OpenSession()) { var question1 = session.Load<Question>("questions/1"); Console.WriteLine(question1.By); } } } } }
اگر برنامه فوق را اجرا کرده و به ساختار اطلاعات ذخیره شده نگاهی بیندازیم به شکل زیر خواهیم رسید:
نکته جالبی که در اینجا وجود دارد، عدم نیاز به join نویسی برای دریافت اطلاعات وابسته به یک شیء است. اگر سؤالی وجود دارد، پاسخهای به آن و یا سایر نظرات، یکجا داخل همان سؤال ذخیره میشوند و به این ترتیب سرعت دسترسی نهایی به اطلاعات بیشتر شده و همچنین قفل گذاری روی سایر اسناد کمتر. این مساله نیز به ذات NoSQL و یا غیر رابطهای RavenDB بر میگردد. در بانکهای اطلاعاتی NoSQL، مفاهیمی مانند کلیدهای خارجی، JOIN بین جداول و امثال آن وجود خارجی ندارند. برای نمونه اگر به کلاسهای مدلهای برنامه دقت کرده باشید، خبری از وجود Id در آنها نیست. RavenDB یک Document store است و نه یک Relation store. در اینجا کل درخت تو در توی خواص یک شیء دریافت و به صورت یک سند ذخیره میشود. به حاصل این نوع عملیات در دنیای بانکهای اطلاعاتی رابطهای، Denormalized data هم گفته میشود.
البته میتوان به کلاسهای تعریف شده خاصیت رشتهای Id را نیز اضافه کرد. در این حالت برای مثال در حالت فراخوانی متد Load، این خاصیت رشتهای، با Id تولید شده توسط RavenDB مانند "questions/1" مقدار دهی میشود. اما از این Id برای تعریف ارجاعات به سؤالات و پاسخهای متناظر استفاده نخواهد شد؛ چون تمام آنها جزو یک سند بوده و داخل آن قرار میگیرند.
namespace jqGrid10.Models { public class Post { public int Id { set; get; } public string Title { set; get; } public string CategoryName { set; get; } public int NumberOfViews { set; get; } } }
- گروه بندی بر روی ستون CategoryName انجام شود.
- ستونی که بر روی آن گروه بندی انجام میشود، نمایش داده نشود.
- در ابتدای نمایش گروهها، تمام آنها به صورت جمع شده و Collapsed نمایش داده شوند.
- پس از نمایش گروهها، اولین گروه به صورت خودکار باز شود.
- تعداد ردیف هر گروه به عنوان گروه اضافه شود.
- جمع کل ستون تعداد بار مشاهدات هر گروه قابل محاسبه شود.
- جمع کل هر گروه در زمانیکه هر گروه نیز بستهاست نمایش داده شود.
- رنگ ردیف جمع کل قابل تنظیم باشد.
فعال سازی گروه بندی در jqGrid
فعال سازی گروه بندی در jqGrid به سادگی افزودن تعاریف ذیل است:
$('#list').jqGrid({ caption: "آزمایش دهم", //... grouping: true, groupingView: { groupField: ['CategoryName'], groupOrder: ['asc'], groupText : ['<b>{0} - {1} ردیف</b>'], groupDataSorted: true, groupColumnShow: false, groupCollapse: true, groupSummary: [true], showSummaryOnHide: true } });
groupField بیانگر آرایهای از ستونهایی است که قرار است بر روی آنها گروه بندی صورت گیرد.
groupOrder آرایهای اختیاری از مقادیر asc یا desc است که متناظر هستند با نحوهی مرتب سازی پیش فرض ستونهای معرفی شده در آرایه groupField.
groupText آرایهای اختیاری از عناوین گروه بندیهای انجام شدهاست. اگر ذکر شود، {0} آن با نام گروه و {1} آن با تعداد عناصر گروه جایگزین میشود.
تنظیم groupDataSorted سبب خواهد شد تا نام ستونی که بر روی آن گروه بندی صورت میگیرد، به سرور ارسال شود (توسط پارامتر sidx). به این ترتیب در سمت سرور میتوان اطلاعات را به صورت پویا مرتب سازی کرده و بازگشت داد.
با تنظیم groupColumnShow به false سبب خواهیم شد تا ستونهای معرفی شده در قسمت groupField نمایش داده نشوند.
با تنظیم groupCollapse به true، در ابتدای نمایش گروهها، ردیفهای آنها نمایش داده نخواهند شد و در حالت جمع شده قرار میگیرند.
groupSummary به معنای فعال سازی نمایش ردیف محاسبهی summary مانند sum، min، max و امثال آن بر روی یک گروه است.
اگر مقدار showSummaryOnHide مساوی true باشد، ردیف محاسبهی summary حتی در حالت groupCollapse: true نمایش داده خواهد شد.
فعال سازی محاسبهی جمع ستون تعداد بار مشاهدات مطالب
برای فعال سازی نهایی محاسبهی جمع ستون تعداد بار مشاهدات، علاوه بر تنظیم groupSummary به true، نیاز است در همان ستون مشخص کنیم که این محاسبات چگونه باید انجام شوند:
colModel: [ // ........ { name: '@(StronglyTyped.PropertyName<Post>(x => x.Title))', index: '@(StronglyTyped.PropertyName<Post>(x => x.Title))', align: 'right', width: 150, summaryTpl: '<div style="text-align: left;">خلاصه </div>', summaryType: function (val, name, record) { return ""; } }, // ........ { name: '@(StronglyTyped.PropertyName<Post>(x => x.NumberOfViews))', index: '@(StronglyTyped.PropertyName<Post>(x => x.NumberOfViews))', align: 'center', width: 70, summaryType: 'sum', summaryTpl: '<b>جمع مشاهدات: {0}</b>' } ],
summaryType مانند ستون عنوان، سفارشی شده نیز میتوان باشد. در ردیف summary و ستون عنوان تنها میخواهیم یک مقدار ثابت را نمایش دهیم، به همین جهت summaryType آن به یک مقدار خالی تنظیم شدهاست.
تغییر رنگ ردیف خلاصه عملیات هر گروه به همراه گشودن خودکار اولین گروه
گروه بندی به همراه یک سری متد توکار نیز هست. برای مثال اگر متد groupingToggle را بر روی Id هر گروه فراخوانی کنیم، میتوان سبب باز یا بسته شده آن گروه شد. متدهای دیگری مانند groupingGroupBy برای گروه بندی پویا و groupingRemove برای حذف گروه بندی نیز وجود دارند:
$('#list').jqGrid({ caption: "آزمایش دهم", //......... loadComplete: function() { //......... $('#list').jqGrid('groupingToggle', 'list' + 'ghead_0_0'); $("tr.jqfoot td").css({ "background": "#2f4f4f", "color": "#FFF" }); }, });
مثال کامل این قسمت را از اینجا میتوانید دریافت کنید:
jqGrid10.zip
OpenCVSharp #8
به تصویر زیر دقت کنید:
فرض کنید در اینجا قصد دارید تعداد توپهای قرمز را شمارش کنید. از دیدگاه یک انسان، شاید سه توپ قرمز قابل مشاهده باشد. اما از دیدگاه یک برنامه، توپ وسطی به دو توپ تفسیر خواهد شد و همچنین نویزهای قرمزی که بین توپها در صفحه وجود دارند نیز شمارش میشوند. بنابراین بهتر است پیش از پردازش این تصویر، ریخت شناسی آنرا بهبود بخشید. برای مثال توپ وسطی را یکی کرد،حفرههای توپهای دیگر را پوشاند و یا نویزهای قرمز را حذف نمود. به علاوه خطوط آبی رنگی را که با یکدیگر تماس یافتهاند نیز میخواهیم اندکی از هم جدا کنیم.
متدهایی که مورفولوژی تصاویر را تغییر میدهند
در OpenCV سه متد یا فیلتر مهم، کار تغییر مورفولوژی تصاویر را انجام میدهند:
1) Cv2.Erode
تحلیل/فرسایش یا erosion سبب میشود تا نواحی تیرهی تصویر «رشد» کنند.
در اینجا فیلتر Erode کار یکی کردن اجزای جدای توپهای رنگی را انجام دادهاست.
2) Cv2.Dilate
اتساع یا dilation سبب خواهد شد تا نواحی روشن تصویر «رشد» کنند.
بکارگیری فیلتر Dilate سبب شدهاست تا نویزهای تصویر محو شوند و اشیاء به هم پیوسته از هم جدا گردند.
3) Cv2.MorphologyEx
کار این متد انجام اعمال پیشرفتهی مورفولوژی بر روی تصاویر است و در اینجا ترکیبی از erosion و dilation، با هم انجام میشوند.
اگر پارامتر سوم آن به MorphologyOperation.Open تنظیم شود، ابتدا erosion و سپس dilation انجام خواهد شد:
و اگر این پارامتر به MorphologyOperation.Close مقدار دهی شود، ابتدا dilation و سپس erosion انجام میشود:
در تمام این حالات، پارامتر آخر که Structuring Element نام دارد، یکی از مقادیر اشیاء مستطیل، به علاوه و بیضی را میتواند داشته باشد. این اشیاء و اندازهی آنها، مشخص کنندهی میزان تحلیل و یا اتساع نهایی هستند.
استفاده از متدهای مورفولوژی در عمل
در اینجا مثالی را از نحوهی بکارگیری متدهای اتساع و فرسایش، ملاحظه میکنید:
using (var src = new Mat(@"..\..\Images\cvmorph.Png", LoadMode.AnyDepth | LoadMode.AnyColor)) using (var dst = new Mat()) { src.CopyTo(dst); var elementShape = StructuringElementShape.Rect; var maxIterations = 10; var erodeDilateWindow = new Window("Erode/Dilate", image: dst); var erodeDilateTrackbar = erodeDilateWindow.CreateTrackbar( name: "Iterations", value: 0, max: maxIterations * 2 + 1, callback: pos => { var n = pos - maxIterations; var an = n > 0 ? n : -n; var element = Cv2.GetStructuringElement( elementShape, new Size(an * 2 + 1, an * 2 + 1), new Point(an, an)); if (n < 0) { Cv2.Erode(src, dst, element); } else { Cv2.Dilate(src, dst, element); } Cv2.PutText(dst, (n < 0) ? string.Format("Erode[{0}]", elementShape) : string.Format("Dilate[{0}]", elementShape), new Point(10, 15), FontFace.HersheyPlain, 1, Scalar.Black); erodeDilateWindow.Image = dst; }); Cv2.WaitKey(); erodeDilateWindow.Dispose(); }
کار متد PutText، نوشتن یک متن ساده بر روی پنجرهی native مربوط به OpenCV است.
همچنین در ادامه کدهای بکارگیری متد MorphologyEx را که کار ترکیب اتساع و فرسایش را با هم انجام میدهد، ذکر شدهاست و نکات بکارگیری آن همانند مثال اول بحث است:
using (var src = new Mat(@"..\..\Images\cvmorph.Png", LoadMode.AnyDepth | LoadMode.AnyColor)) using (var dst = new Mat()) { src.CopyTo(dst); var elementShape = StructuringElementShape.Rect; var maxIterations = 10; var openCloseWindow = new Window("Open/Close", image: dst); var openCloseTrackbar = openCloseWindow.CreateTrackbar( name: "Iterations", value: 0, max: maxIterations * 2 + 1, callback: pos => { var n = pos - maxIterations; var an = n > 0 ? n : -n; var element = Cv2.GetStructuringElement( elementShape, new Size(an * 2 + 1, an * 2 + 1), new Point(an, an)); if (n < 0) { Cv2.MorphologyEx(src, dst, MorphologyOperation.Open, element); } else { Cv2.MorphologyEx(src, dst, MorphologyOperation.Close, element); } Cv2.PutText(dst, (n < 0) ? string.Format("Open/Erosion followed by Dilation[{0}]", elementShape) : string.Format("Close/Dilation followed by Erosion[{0}]", elementShape), new Point(10, 15), FontFace.HersheyPlain, 1, Scalar.Black); openCloseWindow.Image = dst; }); Cv2.WaitKey(); openCloseWindow.Dispose(); }
کدهای کامل این مثال را از اینجا میتوانید دریافت کنید.
در صورتی که ویژوال استودیوی شما دارای این ورژن و آپدیت نبود، میتوانید چارچوب دات نت 4.6.1 را جداگانه در سیستم خود نصب نمایید. توجه داشته باشید که برای استفاده از چارچوب دات نت در ویژوال استودیو باید نسخههای DevPack یا DeveloperPack را نصب نمایید (دریافت دات نت 4.6.1 نسخه مخصوص استفاده در ویژال استودیو).
در پروژه ایجاد شده فایلی به نام Program.cs و در آن کلاس Program وجود دارد. در این کلاس تابع شروع کننده برنامه یعنی Main وجود دارد و برنامه از این تابع شروع خواهد شد.
نمایی از فایلهای پروژه
در تابع شروع کننده برنامه ابتدا وضعیت پشتیبانی از SIMD را چک میکنیم. این کار را همانطور که قبلا در مقاله پیشنیاز توضیح داده شده است با استفاده از خاصیت Vector.IsHardwareAccelerated بررسی میکنیم. اگر مقدار آن برابر با False باشد به معنای عدم پشتیبانی میباشد و با بررسی این موضوع در اول برنامه، در صورت عدم پشتیبانی از SIMD به اجرای ادامهی برنامه خاتمه میدهیم.
پس از بررسی وضعیت پشتیبانی از SIMD ، تابعی را که در فایل Utilities.cs نوشته شده است، فراخوانی میکنیم. این تابع به بررسی وضعیت تعداد رجیسترهای SIMD و وضعیت انواع نوعهای دادهای در SIMD میپردازد. اگر هر نوع دادهای از SIMD پشتیبانی کند (که بستگی به نوع پردازنده شما دارد) اندازه هر نوع دادهای را در SIMD چاپ میکند و در صورت عدم پشتیبانی هر نوع دادهای از SIMD مقدار «عدم پشتیبانی SIMD از آن نوع دادهای» چاپ خواهد شد.
using System.Numerics; using static System.Console; namespace TestSIMD { class Program { private const int ArraySize = 7680 * 4320; static void Main(string[] args) { // بررسی وضعیت پشتیبانی از SIMD if (!Vector.IsHardwareAccelerated) { WriteLine("Hardware acceleration not supported."); WriteLine(); return; // عدم پشتیبانی و خاتمه برنامه } WriteLine("Hardware acceleration is supported"); // اعلام پشتیبانی از SIMD WriteLine(); // بررسی وضعیت نوعهای داده ای در مشخصات سخت افزاری SIMD Utilities.PrintHardwareSpecificSimdEffectiveness(); //به منظور عدم خروج از برنامه و دیدن نتایج آزمایش WriteLine("Press any key to exit"); ReadKey(); } } }
- فایل IntSimdProcessor.cs
- در این فایل کلاسی به نام IntSimdProcessor قرار دارد که شامل 6 تابع میباشد و این تابعها با نوع دادهای صحیح یا همان Integer کار میکنند. نام کلاس هم به همین خاطر نام گذاری شده است.
- این 6 تابع در کل 3 عملیات را شامل عملیاتهای زیر انجام میدهند. یکبار در حالت معمولی و یکبار با استفاده از توابع SIMD این کار را انجام میدهند:
- پیدا کردن بزرگترین و کوچکترین عدد در آرایه
- جمع عناصر دو آرایه با هم با استفاده از یک آرایه کمکی که نتیجه در آرایه کمکی ریخته میشود
- جمع عناصر دو آرایه بدون استفاده از آرایه کمکی که مجموع در آرایه اول ریخته میشود
- در بالای هر تابع در این فایل توضیحات لازم دربارهی فعالیت آن تابع ذکر شده است.
- فایل FloatSimdProcessor.cs
- در این فایل کلاسی با نام FloatSimdProcessor قرار دارد که همانطور که از نام کلاس پیداست، توابعی برای کار بر روی اعداد از نوع دادهای float در آن نوشته شدهاند.
- در این کلاس هم 6 تابع برای انجام 3 عملیات زیر نوشته شده است که به ازای هر عملیات دو تابع یکی در حالت معمولی و یکی در حالت SIMD نوشته شده است.
- جمع دو آرایه با استفاده از یک آرایه کمکی - مجموع در آرایه کمکی ریخته میشود
- جمع دو آرایه اول ورودی - مجموع در آرایه سوم ریخته میشود
- جمع دو آرایه بدون استفاده از آرایه کمکی - مجموع در آرایه اول ریخته میشود
- در آزمایشات نوشته شده در کلاس PerformanceTests تنها از عملیات آخری استفاده شده است و از دو عملیات اول استفاده نشده است که در صورت تمایل میتوانید از دیگر عملیاتها نیز استفاده کنید.
- در بالای هر تابع در این فایل توضیحات لازم دربارهی فعالیت آن تابع نیز ذکر شده است.
- فایل UShortSimdProcessor.cs
- در این فایل کلاسی با نام UShortSimdProcessor قرار دارد و همانطور که از نام کلاس پیداست، توابعی برای کار بر روی اعداد از نوع دادهای ushort یا همان اعداد صحیح کوچک بدون علامت نوشته شدهاند.
- در این کلاس 12 تابع برای انجام 6 عملیات زیر نوشته شدهاست که به ازای هر عملیات، دو تابع یکی در حالت معمولی و یکی در حالت SIMD نوشته شده است.
- جمع دو آرایه اول ورودی که مجموع در آرایه سوم ریخته میشود
- جمع دو آرایه بدون استفاده از آرایه کمکی که مجموع در آرایه اول ریخته میشود
- بدست آوردن کمترین و بیشترین مقدار در یک آرایه اعداد صحیح کوچک بدون علامت
- جمع عناصر آرایه ورودی و ذخیره مجموع آنها در یک متغیر کمکی
- جمع عناصر آرایه ورودی و ذخیره مجموع آنها در یک متغیر کمکی بدون بررسی سرریز (Overflow)
- محاسبه میانگین و بدست آوردن کمترین و بیشترین مقدار در یک آرایه اعداد صحیح کوچک بدون علامت
- در بالای هر تابع در این فایل توضیحات لازم دربارهی فعالیت آن تابع ذکر شده است.
public static void TestIntArrayAdditionFunctions(int testSetSize) { WriteLine(); Write("Testing int array addition, generating test data..."); var intsOne = GetRandomIntArray(testSetSize); //تولید آرایه عددی به صورت تصادفی var intsTwo = GetRandomIntArray(testSetSize); WriteLine($" done, testing...");// پایان تولید آرایهها و شروع پردازش var naiveTimesMs = new List<long>(); // تعریف لیستی برای ریختن زمان پاسخ دهی در حالت ساده و معمولی var hwTimesMs = new List<long>(); // تعریف لیستی برای ریختن زمان پاسخ دهی در حالت SIMD و سخت افزاری for (var i = 0; i < 3; i++) { // ایجاد حلقه برای تکرار محاسبات برای اندازه گیری زمان در حالت تکراری stopwatch.Restart();//شروع ثبت زمان var result = IntSimdProcessor.NaiveSumFunc(intsOne, intsTwo);//اجرای تابع جمع دو آرایه var naiveTimeMs = stopwatch.ElapsedMilliseconds;//ثبت زمان naiveTimesMs.Add(naiveTimeMs);//افزودن زمان ثبت شده به لیست زمانهای ساده و معمول WriteLine($"Naive analysis took: {naiveTimeMs}ms (last value = {result.Last()})."); stopwatch.Restart();//شروع ثبت زمان result = IntSimdProcessor.HWAcceleratedSumFunc(intsOne, intsTwo);//اجرای تابع جمع دو آرایه در حالت سخت افزاری var hwTimeMs = stopwatch.ElapsedMilliseconds;//ثبت زمان hwTimesMs.Add(hwTimeMs);//افزودن زمان به لیست زمانهای سخت افزاری WriteLine($"Hareware accelerated analysis took: {hwTimeMs}ms (last value = {result.Last()})."); }//پایان حلقه و چاپ نتایج WriteLine("Int array addition:"); WriteLine($"Naive method average time: {naiveTimesMs.Average():.##}"); WriteLine($"HW accelerated method average time: {hwTimesMs.Average():.##}"); WriteLine($"Hardware speedup: {naiveTimesMs.Average() / hwTimesMs.Average():P}%"); }
نام تابع ذکر شده نشان دهنده آزمایش بر روی آرایه اعداد صحیح یا همان Integer میباشد که شامل یک پارامتر ورودی از نوع عدد صحیح میباشد. این پارامتر ورودی نشان دهنده اندازه هر آرایهای میباشد که قرار است تولید شود.
TestIntArrayAdditionFunctions(int testSetSize)
در قدم اول این تابع، باید آرایهها را تولید کنیم که کد آن به صورت زیر است.
Write("Testing int array addition, generating test data..."); var intsOne = GetRandomIntArray(testSetSize); var intsTwo = GetRandomIntArray(testSetSize); WriteLine($" done, testing...");
ابتدا در خروجی چاپ میکنیم که در حال ایجاد دادههای مربوط به آزمایش هستیم و سپس با استفاده از تابع GetRandomIntArray آرایهای را ایجاد میکنیم و در متغیرهای مربوطه میریزیم. این تابع دارای یک پارامتر ورودی از نوع عدد صحیح است که آرایهای را به طول پارامتر ورودی تولید میکند. این تابع در فایل Utilities.cs قرار دارد.
در پایان تولید آرایهها، اتمام تولید و ایجاد آرایهها را با چاپ در خروجی اعلام میکنیم.
سپس با معرفی دو لیست زیر میتوانیم زمانهای اجرا را در آنها بریزیم و در پایان، تابع میانگین این زمانها را محاسبه و چاپ کنیم. لیست اول برای نگهداری زمانهای اجرای عملیات در حالت معمولی و لیست دوم برای نگهداری زمانهای اجرای عملیات در حالت SIMD میباشد.
var naiveTimesMs = new List<long>(); var hwTimesMs = new List<long>();
سپس با ایجاد حلقه ای از 0 تا 3 که در کل 3 مرتبه اجرا میشود عملیات را تکرار و زمان آن را ثبت میکنیم.
for (var i = 0; i < 3; i++)
درون حلقه یک عملیات را در دوحالت معمولی یا ساده و SIMD اجرا میکنیم. قبل از اجرای عملیات اول ابتدا stopwatch را ریست میکنیم. با این کار زمان صفر شده و شروع به اندازه گیری میکند. سپس عملیات مربوط به جمع دو آرایه را در حالت معمولی که در فایل IntSimdProcessor.cs قرار دارد، فراخوانی میکنیم. پس از اجرای این عملیات مقدار stopwatch را به میلی ثانیه در یک متغیر ذخیره میکنیم و این مقدار را به لیست زمانهای اجرای معمولی اضافه میکنیم. در نهایت نتیجه زمان اجرا را در خروجی چاپ میکنیم.
stopwatch.Restart(); var result = IntSimdProcessor.NaiveSumFunc(intsOne, intsTwo); var naiveTimeMs = stopwatch.ElapsedMilliseconds; naiveTimesMs.Add(naiveTimeMs); WriteLine($"Naive analysis took: {naiveTimeMs}ms (last value = {result.Last()}).");
پس از اجرای عملیات در حالت ساده یا معمولی، حال نوبت همان عملیات در حالت SIMD میباشد. دوباره stopwatch را ریست میکنیم و عملیات در SIMD را اجرا کرده و بعد از آن مقدار stopwatch را درون متغیری میریزیم و آن را به لیست زمانهای اجرای عملیات در SIMD اضافه میکنیم و در نهایت نتیجه زمان اجرا را در خروجی چاپ میکنیم.
stopwatch.Restart(); result = IntSimdProcessor.HWAcceleratedSumFunc(intsOne, intsTwo); var hwTimeMs = stopwatch.ElapsedMilliseconds; hwTimesMs.Add(hwTimeMs); WriteLine($"Hareware accelerated analysis took: {hwTimeMs}ms (last value = {result.Last()}).");
پس از اجرای حلقه، حال نوبت به نمایش نتیجه میانگین زمانها در خروجی است. ابتدا میانگین زمانهای اجرا در حالت ساده یا معمولی را که به میلی ثانیه است را در خروجی چاپ میکنیم. بعد از آن میانگین زمانهای اجرا در حالت SIMD را در خروجی چاپ میکنیم و در آخر سرعت زمان اجرا در حالت SIMD را نسبت به حالت معمولی به درصد چاپ میکنیم.
WriteLine($"Naive method average time: {naiveTimesMs.Average():.##}"); WriteLine($"HW accelerated method average time: {hwTimesMs.Average():.##}"); WriteLine($"Hardware speedup: {naiveTimesMs.Average() / hwTimesMs.Average():P}%");
در این مقاله تنها به توضیحی در مورد این آزمایش اکتفا میکنیم. لازم به ذکر است که دیگر آزمایشها نیز دقیقا ساختاری مشابه این آزمایش را دارند و تنها عملیات اجرا در آنها متفاوت است. در کلاس PerformanceTests توضیحات لازم مربوط به هر آزمایش و تابع داده شده است و میتوانید با مراجعه به کد برنامه آنها را مورد بررسی قرار دهید.
برای اجرای تمامی آزمایشها، کلیه توابع نوشته شده در کلاس PerformanceTests را در کلاس Program و در تابع Main که تابع شروع کننده برنامه میباشد، پس از بررسی وضعیت نوعهای دادهای قرار میدهیم.
تصویر مربوط به اجرای کامل برنامه را میتوانید مشاهده میکنید.
این جدول بر اساس یک بار اجرای برنامه در سیستم من ترسیم شده است و اجرای برنامه در سیستمهای مختلف خروجیهای متفاوتی را دارد. لازم به ذکر است که اندازه آرایهها بسیار بزرگ است و این نتایج با آرایههایی به طول بیش از هزاران هزار عنصر میباشد.
زمانها در جدول به میلی ثانیه میباشد.
ردیف | عملیات | دور اول | دور دوم | دور سوم | میانگین حالت ساده | میانگین حالت SIMD | |||
درحالت ساده | درحالت SIMD | درحالت ساده | درحالت SIMD | درحالت ساده | درحالت SIMD | ||||
1 | جمع دو آرایه با استفاده از یک آرایه کمکی در اعداد صحیح | 157 | 131 | 128 | 131 | 128 | 138 | 137.67 | 133.33 |
2 | جمع دو آرایه بدون استفاده از آرایه کمکی در اعداد float | 122 | 133 | 99 | 99 | 99 | 93 | 106.67 | 108.33 |
3 | جمع دو آرایه بدون استفاده از آرایه کمکی در اعداد صحیح | 83 | 73 | 86 | 88 | 78 | 81 | 82.33 | 80.67 |
4 | جمع دو آرایه اول ورودی - مجموع در آرایه سوم ریخته میشود - در اعداد صحیح کوچک بدون علامت | 58 | 63 | 50 | 48 | 58 | 46 | 55.33 | 52.33 |
5 | جمع دو آرایه بدون استفاده از آرایه کمکی در اعداد صحیح کوچک بدون علامت | 55 | 40 | 53 | 36 | 53 | 46 | 53.67 | 40.67 |
6 | بدست آوردن کمترین و بیشترین مقدار در یک آرایه اعداد صحیح | 91 | 36 | 91 | 39 | 90.67 | 38 | 90.66 | 38 |
7 | بدست آوردن کمترین و بیشترین مقدار در یک آرایه اعداد صحیح کوچک بدون علامت | 90 | 20 | 89 | 19 | 88 | 18 | 89 | 19 |
8 | جمع عناصر آرایه ورودی و ذخیره مجموع آنها در یک متغیر کمکی | 33 | 309 | 32 | 263 | 31 | 291 | 32 | 287.67 |
9 | جمع عناصر آرایه ورودی و ذخیره مجموع آنها در یک متغیر کمکی بدون بررسی سرریز | 30 | 13 | 29 | 13 | 30 | 12 | 29.67 | 12.67 |
10 | محاسبه میانگین و بدست آوردن کمترین و بیشترین مقدار در آرایه اعداد صحیح کوچک بدون علامت | 89 | 50 | 90 | 51 | 90 | 49 | 89.57 | 50 |