اشتراکها
اشتراکها
مقاله کامل در مورد Threading در #C
نظرات مطالب
فارسی کردن گرید تلریک در برنامه های ویندوزی
دوست عزیز
راهنمای فارسی کردن کنترلهای عمومی در سایت تلریک و راهنمای فارسی کردن گرید تلریک در برنامههای wpf در دسترس میباشد.
راهنمای فارسی کردن کنترلهای عمومی در سایت تلریک و راهنمای فارسی کردن گرید تلریک در برنامههای wpf در دسترس میباشد.
نظرات مطالب
مروری بر کدهای کلاس SqlHelper
ممنون استاددر این زمینه کتابهای خوب فارسی هم سراغ دارید؟
همچنین برای یادگیری MVC چه کتاب فارسی رو پیشنهاد میدید؟
همچنین برای یادگیری MVC چه کتاب فارسی رو پیشنهاد میدید؟
جهت اطلاع، چند روز قبل پروژهای را در سایت CodePlex دیدم در مورد فارسی سازی و فارسی ساز SharePoint Foundation 2010 :
پاسخ به بازخوردهای پروژهها
چند متد الحاقی SEO
مباحث eager fetching/loading (واکشی حریصانه) و lazy loading/fetching (واکشی در صورت نیاز، با تاخیر، تنبل) جزو نکات کلیدی کار با ORM های پیشرفته بوده و در صورت عدم اطلاع از آنها و یا استفادهی ناصحیح از هر کدام، باید منتظر از کار افتادن زود هنگام سیستم در زیر بار چند کاربر همزمان بود. به همین جهت تصور اینکه "با استفاده از ORMs دیگر از فراگیری SQL راحت شدیم!" یا اینکه "به من چه که پشت صحنه چه اتفاقی میافته!" بسی مهلک و نادرست است!
در ادامه به تفصیل به این موضوع پرداخته خواهد شد.
ابزار مورد نیاز
در این مطلب از برنامهی NHProf استفاده خواهد شد.
اگر مطالب NHibernate این سایت را دنبال کرده باشید، در مورد لاگ کردن SQL تولیدی به اندازهی کافی توضیح داده شده یا حتی یک ماژول جمع و جور هم برای مصارف دم دستی نوشته شده است. این موارد شاید این ایده را به همراه داشته باشند که چقدر خوب میشد یک برنامهی جامعتر برای این نوع بررسیها تهیه میشد. حداقل SQL نهایی فرمت میشد (یعنی برنامه باید مجهز به یک SQL Parser تمام عیار باشد که کار چند ماهی هست ...؛ با توجه به اینکه مثلا NHibernate از افزونههای SQL ویژه بانکهای اطلاعاتی مختلف هم پشتیبانی میکند، مثلا T-SQL مایکروسافت با یک سری ریزه کاریهای منحصر به MySQL متفاوت است)، یا پس از فرمت شدن، syntax highlighting به آن اضافه میشد، در ادامه مشخص میکرد کدام کوئریها سنگینتر هستند، کدامیک نشانهی عدم استفادهی صحیح از ORM مورد استفاده است، چه مشکلی دارد و از این موارد.
خوشبختانه این ایدهها یا آرزوها با برنامهی NHProf محقق شده است. این برنامه برای استفادهی یک ماه اول آن رایگان است (آدرس ایمیل خود را وارد کنید تا یک فایل مجوز رایگان یک ماهه برای شما ارسال گردد) و پس از یک ماه، باید حداقل 300 دلار هزینه کنید.
واکشی حریصانه و غیرحریصانه چیست؟
رفتار یک ORM جهت تعیین اینکه آیا نیاز است برای دریافت اطلاعات بین جداول Join صورت گیرد یا خیر، واکشی حریصانه و غیرحریصانه را مشخص میسازد.
در حالت واکشی حریصانه به ORM خواهیم گفت که لطفا جهت دریافت اطلاعات فیلدهای جداول مختلف، از همان ابتدای کار در پشت صحنه، Join های لازم را تدارک ببین. در حالت واکشی غیرحریصانه به ORM خواهیم گفت به هیچ عنوان حق نداری Join ایی را تشکیل دهی. هر زمانی که نیاز به اطلاعات فیلدی از جدولی دیگر بود باید به صورت مستقیم به آن مراجعه کرده و آن مقدار را دریافت کنی.
به صورت خلاصه برنامه نویس در حین کار با ORM های پیشرفته نیازی نیست Join بنویسد. تنها باید ORM را طوری تنظیم کند که آیا اینکار را حتما خودش در پشت صحنه انجام دهد (واکشی حریصانه)، یا اینکه خیر، به هیچ عنوان SQL های تولیدی در پشت صحنه نباید حاوی Join باشند (lazy loading).
چگونه واکشی حریصانه و غیرحریصانه را در NHibernate 3.0 تنظیم کنیم؟
در NHibernate اگر تنظیم خاصی را تدارک ندیده و خواص جداول خود را به صورت virtual معرفی کرده باشید، تنظیم پیش فرض دریافت اطلاعات همان lazy loading است. به مثالی در این زمینه توجه بفرمائید:
مدل برنامه:
مدل برنامه همان مثال کلاسیک مشتری و سفارشات او میباشد. هر مشتری چندین سفارش میتواند داشته باشد. هر سفارش به یک مشتری وابسته است. هر سفارش نیز از چندین قلم جنس تشکیل شده است. در این خرید، هر جنس نیز به یک سفارش وابسته است.
using System.Collections.Generic;
namespace CustomerOrdersSample.Domain
{
public class Customer
{
public virtual int Id { get; set; }
public virtual string Name { get; set; }
public virtual IList<Order> Orders { get; set; }
}
}
using System;
using System.Collections.Generic;
namespace CustomerOrdersSample.Domain
{
public class Order
{
public virtual int Id { get; set; }
public virtual DateTime OrderDate { set; get; }
public virtual Customer Customer { get; set; }
public virtual IList<OrderItem> OrderItems { set; get; }
}
}
namespace CustomerOrdersSample.Domain
{
public class OrderItem
{
public virtual int Id { get; set; }
public virtual Product Product { get; set; }
public virtual int Quntity { get; set; }
public virtual Order Order { set; get; }
}
}
namespace CustomerOrdersSample.Domain
{
public class Product
{
public virtual int Id { set; get; }
public virtual string Name { get; set; }
public virtual decimal UnitPrice { get; set; }
}
}
که جداول متناظر با آن به صورت زیر خواهند بود:
create table Customers (
CustomerId INT IDENTITY NOT NULL,
Name NVARCHAR(255) null,
primary key (CustomerId)
)
create table Orders (
OrderId INT IDENTITY NOT NULL,
OrderDate DATETIME null,
CustomerId INT null,
primary key (OrderId)
)
create table OrderItems (
OrderItemId INT IDENTITY NOT NULL,
Quntity INT null,
ProductId INT null,
OrderId INT null,
primary key (OrderItemId)
)
create table Products (
ProductId INT IDENTITY NOT NULL,
Name NVARCHAR(255) null,
UnitPrice NUMERIC(19,5) null,
primary key (ProductId)
)
alter table Orders
add constraint fk_Customer_Order
foreign key (CustomerId)
references Customers
alter table OrderItems
add constraint fk_Product_OrderItem
foreign key (ProductId)
references Products
alter table OrderItems
add constraint fk_Order_OrderItem
foreign key (OrderId)
references Orders
همچنین یک سری اطلاعات آزمایشی زیر را هم در نظر بگیرید: (بانک اطلاعاتی انتخاب شده SQL CE است)
SET IDENTITY_INSERT [Customers] ON;
GO
INSERT INTO [Customers] ([CustomerId],[Name]) VALUES (1,N'Customer1');
GO
SET IDENTITY_INSERT [Customers] OFF;
GO
SET IDENTITY_INSERT [Products] ON;
GO
INSERT INTO [Products] ([ProductId],[Name],[UnitPrice]) VALUES (1,N'Product1',1000.00000);
GO
INSERT INTO [Products] ([ProductId],[Name],[UnitPrice]) VALUES (2,N'Product2',2000.00000);
GO
INSERT INTO [Products] ([ProductId],[Name],[UnitPrice]) VALUES (3,N'Product3',3000.00000);
GO
SET IDENTITY_INSERT [Products] OFF;
GO
SET IDENTITY_INSERT [Orders] ON;
GO
INSERT INTO [Orders] ([OrderId],[OrderDate],[CustomerId]) VALUES (1,{ts '2011-01-07 11:25:20.000'},1);
GO
SET IDENTITY_INSERT [Orders] OFF;
GO
SET IDENTITY_INSERT [OrderItems] ON;
GO
INSERT INTO [OrderItems] ([OrderItemId],[Quntity],[ProductId],[OrderId]) VALUES (1,10,1,1);
GO
INSERT INTO [OrderItems] ([OrderItemId],[Quntity],[ProductId],[OrderId]) VALUES (2,5,2,1);
GO
INSERT INTO [OrderItems] ([OrderItemId],[Quntity],[ProductId],[OrderId]) VALUES (3,20,3,1);
GO
SET IDENTITY_INSERT [OrderItems] OFF;
GO
دریافت اطلاعات :
میخواهیم نام کلیه محصولات خریداری شده توسط مشتریها را به همراه نام مشتری و زمان خرید مربوطه، نمایش دهیم (دریافت اطلاعات از 4 جدول بدون join نویسی):
var list = session.QueryOver<Customer>().List();
foreach (var customer in list)
{
foreach (var order in customer.Orders)
{
foreach (var orderItem in order.OrderItems)
{
Console.WriteLine("{0}:{1}:{2}", customer.Name, order.OrderDate, orderItem.Product.Name);
}
}
}
خروجی به صورت زیر خواهد بود:
Customer1:2011/01/07 11:25:20 :Product1
Customer1:2011/01/07 11:25:20 :Product2
Customer1:2011/01/07 11:25:20 :Product3
همانطور که مشاهده میکنید در اینجا اطلاعات از 4 جدول مختلف دریافت میشوند اما ما Join ایی را ننوشتهایم. ORM هرجایی که به اطلاعات فیلدهای جداول دیگر نیاز داشته، به صورت مستقیم به آن جدول مراجعه کرده و یک کوئری، حاصل این عملیات خواهد بود (مطابق تصویر جمعا 6 کوئری در پشت صحنه برای نمایش سه سطر خروجی فوق اجرا شده است).
این حالت فقط و فقط با تعداد رکورد کم بهینه است (و به همین دلیل هم تدارک دیده شده است). بنابراین اگر برای مثال قصد نمایش اطلاعات حاصل از 4 جدول فوق را در یک گرید داشته باشیم، بسته به تعداد رکوردها و تعداد کاربران همزمان برنامه (خصوصا در برنامههای تحت وب)، بانک اطلاعاتی باید بتواند هزاران هزار کوئری رسیده حاصل از lazy loading را پردازش کند و این یعنی مصرف بیش از حد منابع (IO بالا، مصرف حافظه بالا) به همراه بالا رفتن CPU usage و از کار افتادن زود هنگام سیستم.
کسانی که پیش از این با SQL نویسی خو گرفتهاند احتمالا الان منابع موجود را در مورد نحوهی نوشتن Join در NHibernate زیر و رو خواهند کرد؛ زیرا پیش از این آموختهاند که برای دریافت اطلاعات از دو یا چند جدول مرتبط باید Join نوشت. اما همانطور که پیشتر نیز عنوان شد، اگر با جزئیات کار با NHibernate آشنا شویم، نیازی به Join نویسی نخواهیم داشت. اینکار را خود ORM در پشت صحنه باید و میتواند مدیریت کند. اما چگونه؟
در NHibernate 3.0 با معرفی QueryOver که جایگزینی از نوع strongly typed همان ICriteria API قدیمی است، یا با معرفی Query که همان LINQ to NHibernate میباشد، متدی به نام Fetch نیز تدارک دیده شده است که استراتژیهای lazy loading و eager loading را به سادگی توسط آن میتوان مشخص نمود.
مثال: دریافت اطلاعات با استفاده از QueryOver
var list = session
.QueryOver<Customer>()
.Fetch(c => c.Orders).Eager
.Fetch(c => c.Orders.First().OrderItems).Eager
.Fetch(c => c.Orders.First().OrderItems.First().Product).Eager
.List();
foreach (var customer in list)
{
foreach (var order in customer.Orders)
{
foreach (var orderItem in order.OrderItems)
{
Console.WriteLine("{0}:{1}:{2}", customer.Name, order.OrderDate, orderItem.Product.Name);
}
}
}
پشت صحنه:
اینبار فقط یک کوئری حاصل عملیات بوده و join ها به صورت خودکار با توجه به متدهای Fetch ذکر شده که حالت eager loading آنها صریحا مشخص شده است، تشکیل شدهاند (6 بار رفت و برگشت به بانک اطلاعاتی به یکبار تقلیل یافت).
نکته 1: نتایج تکراری
اگر حاصل join آخر را نمایش دهیم، نتایجی تکراری خواهیم داشت که مربوط است به مقدار دهی customer با سه وهله از شیء مربوطه تا بتواند واکشی حریصانهی مجموعه اشیاء فرزند آنرا نیز پوشش دهد. برای رفع این مشکل یک سطر TransformUsing باید اضافه شود:
...
.TransformUsing(NHibernate.Transform.Transformers.DistinctRootEntity)
.List();
دریافت اطلاعات با استفاده از LINQ to NHibernate3.0
برای اینکه بتوان متدهای Fetch ذکر شده را به LINQ to NHibernate 3.0 اعمال نمود، ذکر فضای نام NHibernate.Linq ضروری است. پس از آن خواهیم داشت:
var list = session
.Query()
.FetchMany(c => c.Orders)
.ThenFetchMany(o => o.OrderItems)
.ThenFetch(p => p.Product)
.ToList();
اینبار از FetchMany، سپس ThenFetchMany (برای واکشی حریصانه مجموعههای فرزند) و در آخر از ThenFetch استفاده خواهد شد.
همانطور که ملاحظه میکنید حاصل این کوئری، با کوئری قبلی ذکر شده یکسان است. هر دو، اطلاعات مورد نیاز از دو جدول مختلف را نمایش میدهند. اما یکی در پشت صحنه شامل چندین و چند کوئری برای دریافت اطلاعات است، اما دیگری تنها از یک کوئری Join دار تشکیل شده است.
نکته 2: خطاهای ممکن
ممکن است حین تعریف متدهای Fetch در زمان اجرا به خطاهای Antlr.Runtime.MismatchedTreeNodeException و یا Specified method is not supported و یا موارد مشابهی برخورد نمائید. تنها کاری که باید انجام داد جابجا کردن مکان بکارگیری extension methods است. برای مثال متد Fetch باید پس از Where در حالت استفاده از LINQ ذکر شود و نه قبل از آن.
پاسخ به بازخوردهای پروژهها
استفاده از فونت های مختلف در گزارش
در قسمت DefaultFonts ، شما میتونید مسیر دهی کامل انجام بدید. مهم نیست که فونت روی سرور نصب است یا خیر:
فونت اول را نازنین معرفی کنید و فونت دوم را times. بقیه آن خودکار است. فونت نازنین دارای حروف انگلیسی نیست. به همین جهت برای متون انگلیسی به صورت خودکار از فونت دوم استفاده خواهد شد.
.DefaultFonts(fonts => { fonts.Path(Environment.GetEnvironmentVariable("SystemRoot") + "\\fonts\\tahoma.ttf", Environment.GetEnvironmentVariable("SystemRoot") + "\\fonts\\verdana.ttf"); })
نظرات مطالب
تنظیمات JSON در ASP.NET Web API
یک نکتهی تکمیلی
اگر نمیخواهید یک وابستگی جدید را (Microsoft.AspNet.WebApi.Client) به پروژه اضافه کنید، کدهای ذیل همان کار HttpClient را برای ارسال اطلاعات، انجام میدهند. کلاس WebRequest آن در فضای نام System.Net موجود است :
اگر نمیخواهید یک وابستگی جدید را (Microsoft.AspNet.WebApi.Client) به پروژه اضافه کنید، کدهای ذیل همان کار HttpClient را برای ارسال اطلاعات، انجام میدهند. کلاس WebRequest آن در فضای نام System.Net موجود است :
using System; using System.IO; using System.Net; using Newtonsoft.Json; namespace WebToolkit { public class SimpleHttp { public HttpStatusCode PostAsJson(string url, object data, JsonSerializerSettings settings) { if (string.IsNullOrWhiteSpace(url)) throw new ArgumentNullException("url"); return PostAsJson(new Uri(url), data, settings); } public HttpStatusCode PostAsJson(Uri url, object data, JsonSerializerSettings settings) { if (url == null) throw new ArgumentNullException("url"); var postRequest = (HttpWebRequest)WebRequest.Create(url); postRequest.Method = "POST"; postRequest.UserAgent = "SimpleHttp/1.0"; postRequest.ContentType = "application/json; charset=utf-8"; using (var stream = new StreamWriter(postRequest.GetRequestStream())) { var serializer = JsonSerializer.Create(settings); using (var writer = new JsonTextWriter(stream)) { serializer.Serialize(writer, data); writer.Flush(); } } using (var response = (HttpWebResponse)postRequest.GetResponse()) { return response.StatusCode; } } } }
فرض کنید قصد دارید عملیات نرمال سازی اطلاعات را بر روی یک رشته انجام داده و برای مثال اعداد فارسی و انگلیسی موجود در یک رشته را یکدست کنید. اولین روشی که برای اینکار به ذهن میرسد، استفاده از متد Replace است:
اما آیا این روش، کارآیی مناسبی را به همراه دارد؟ در ادامه چند روش دیگر را نیز جهت جایگزین کردن حروف، معرفی کرده و کارآیی آنها را با هم مقایسه میکنیم.
جایگزین کردن حروف با استفاده از Replace معمولی توسط رشتهها
نگارش اصلی تبدیل تمام اعداد موجود در یک رشته به اعداد فارسی، به صورت زیر است که در آن یک دست سازی اعداد عربی هم درنظر گرفته شدهاند (برای مثال طرز نگارش عدد 4 فارسی و عربی متفاوت است):
جایگزین کردن حروف با استفاده از Replace معمولی توسط کاراکترها
اینبار همان حالت قبل را درنظر بگیرید؛ با این تفاوت که بجای رشتهها از کاراکترها استفاده شود. برای مثال بجای:
خواهیم داشت:
جایگزین کردن حروف با استفاده از String Builder
در ادامه بجای استفاده از متد Replace متداول، آرایهای از حروف قابل جایگزینی را توسط یک StringBuilder ایجاد کرده و حروف را یکی یکی تبدیل میکنیم و به این ترتیب برخلاف متد Replace، هربار برای جایگزینی یک مورد خاص، مجددا از ابتدای رشته شروع به جستجو نمیشود:
جایگزین کردن حروف با استفاده از ToCharArray
متد زیر دقیقا شبیه به حالت استفاده از String Builder است؛ با یک تفاوت مهم: بجای استفاده از String Builder برای تهیهی آرایهای از حروف قابل تغییر، از متد ToCharArray استفاده شدهاست:
جایگزین کردن حروف با استفاده از string.Create
string.Create یکی از تازههای NET Core. است که امکان تغییر مستقیم یک قطعه string را میسر میکند:
در کدهای فوق، ابتدا طول رشتهی نهایی بازگشتی از string.Create مشخص میشود. سپس توسط پارامتر دوم، دادههایی که قرار است بر روی آنها کاری صورت گیرد به متد string.Create ارسال میشوند. در آخر عملیات نهایی در action delegate تعریف شده رخ میدهد. در اینجا chars، به بافر درونی رشتهای که بازگشت داده میشود، اشاره میکند و باید پر شود (این بافر مستقیما در دسترس است). context همان پارامتر دوم متد string.Create است.
توضیحات بیشتر:
در دات نت، رشتهها نوعهای ارجاعی (reference type) غیرقابل تغییر (immutable) هستند. به این معنا که هر زمانیکه ایجاد شدند، دیگر نمیتوان محتوای آنها را تغییر داد. به همین جهت است که مجبور هستیم آنها را برای مثال توسط ToCharArray به یک آرایه تبدیل کنیم و سپس این آرایهی قابل تغییر را ویرایش کنیم. در حین کار با رشتهها، این غیرقابل تغییر بودن، سبب تخصیص حافظههای بیش از حدی میشوند. اگر بخواهیم قسمتی از یک رشته را جدا و یا جایگزین کنیم و یا تعدادی رشته را با هم جمع بزنیم، نتیجهی آن نیاز به یک تخصیص حافظهی جدید را دارد. راه حل استاندارد مواجه شدن با این مشکل، استفاده از StringBuilder است که از یک بافر داخلی برای انجام کارهای خودش استفاده میکند و زمانیکه نتیجهی نهایی را از آن درخواست میکنیم، تخصیص حافظهای را برای تولید رشتهی حاصل انجام میدهد. البته این مورد نیاز به اندازه گیری دارد و ارزش StringBuilder با حجم بالایی از اطلاعات متنی مشخص میشود؛ وگرنه همانطور که مشاهده میکنید (در نتیجهی نهایی بحث در ادامه)، الزاما کدهای سریعتری را به همراه نخواهد داشت.
هدف از string.Create، ایجاد رشتهها از دادههای موجود است. هدف اصلی آن کاهش تخصیصهای حافظه و کپی کردن اطلاعات است و امضای آن به صورت زیر میباشد:
مزیت این متد، عدم نیاز به یک پیشبافر است؛ به این معنا که مستقیما بر روی قسمتی از حافظه کار میکند که ارجاعی را به رشتهی «بازگشتی» دارد. یعنی در حالت کار با string.Create، غیرقابل تغییر بودن رشتهها در دات نت دیگر صادق نخواهد بود و برای تغییر آن نیازی به تخصیص بافر، کپی کردن و تخصیص حافظهی نهایی برای بازگشت نتیجه نیست. پارامتر SpanAction آن، امکان دسترسی مستقیم به این ناحیهی از حافظه را میسر میکند.
هنگام کار با این متد، chars ای که در اختیار ما قرار میگیرد، یک <Span<char اشاره کننده به رشتهی نهایی است که قرار است بازگشت داده شود (در ابتدای کار بر اساس اندازهای که مشخص میشود، یک رشتهی خالی تخصیص داده میشود، اما بافر پر کردن آن اینبار در دسترس است و نیازی به تخصیص و کپی جداگانهای را ندارد). بنابراین روش کار با این متد، پر کردن بافر درونی رشتهی بازگشتی (همان chars در اینجا) به صورت مستقیم است؛ کاری که با یک رشتهی معمولی نمیتوان انجام داد.
State یا همان پارامتر دوم این متد، هر چیزی میتواند باشد. اگر نیاز است چندین رشته را در اینجا دریافت کنید تا بتوان بر اساس آن رشتهی نهایی را تشکیل داد، یک struct را تعریف کرده و بجای state به آن ارسال کنید. سپس این state توسط پارامتر context مربوط به SpanAction<char, string> action قابل دریافت و استفادهاست که در این مثال، context همان data ارسالی به این متد است.
سؤال: در حین کار با string.Create، باید از پارامتر data استفاده کنیم و یا از context دریافتی؟ به نظر در مثال فوق، data و context یکی هستند. اکنون داخل action delegate مهیا که جهت ساخت رشتهی نهایی بکار میرود، باید از data استفاده کرد و یا از context؟
در اینجا اگر در داخل action delegate، ارجاعی را به data داشته باشیم، یک closure تشکیل میشود و در این حالت کامپایلر برای مدیریت آن، نیاز به تولید یک کلاس را در پشت صحنه خواهد داشت که خودش سبب کاهش کارآیی میگردد. به همین جهت متد Create، پارامتر state را به صورت معمولی دریافت میکند و آنرا توسط context در اختیار delegate قرار میدهد تا نیازی نباشد delegate تعریف شده، یک closure را تشکیل دهد.
نتیجهی نهایی بررسی کارآیی روشهای مختلف جایگزین کردن حروف در یک رشته
کدهای کامل این مطلب را از اینجا میتوانید دریافت کنید: ReplacePerformanceTests.zip
ستون op/s در اینجا، مهمترین ستون گزارش است و به معنای تعداد عملیات قابل انجام در یک ثانیه است. از 670 هزار عملیات در ثانیه با Replace معمولی، به 5 میلیون عملیات در ثانیه رسیدهایم که بسیار قابل توجهاست.
همانطور که مشاهده میکنید، string.Create، سریعترین نگارش موجود است. در این بین نگارشی که از ToCharArray استفاده میکند، قابلیت انتقال بیشتری را دارد؛ از این جهت که نگارشهای دیگر NET. هنوز دسترسی به string.Create را ندارند. همچنین نگارش کاراکتری متد Replace، از متد رشتهای آن سریعتر عمل کردهاست.
private static string toPersianNumbersUsingReplace(string data) { if (string.IsNullOrWhiteSpace(data)) return string.Empty; return data .Replace("0", "\u06F0") .Replace("1", "\u06F1") .Replace("2", "\u06F2") .Replace("3", "\u06F3") .Replace("4", "\u06F4") .Replace("5", "\u06F5") .Replace("6", "\u06F6") .Replace("7", "\u06F7") .Replace("8", "\u06F8") .Replace("9", "\u06F9"); }
جایگزین کردن حروف با استفاده از Replace معمولی توسط رشتهها
نگارش اصلی تبدیل تمام اعداد موجود در یک رشته به اعداد فارسی، به صورت زیر است که در آن یک دست سازی اعداد عربی هم درنظر گرفته شدهاند (برای مثال طرز نگارش عدد 4 فارسی و عربی متفاوت است):
private static string toPersianNumbersUsingReplace(string data) { if (string.IsNullOrWhiteSpace(data)) return string.Empty; return toEnglishNumbers(data) .Replace("0", "\u06F0") .Replace("1", "\u06F1") .Replace("2", "\u06F2") .Replace("3", "\u06F3") .Replace("4", "\u06F4") .Replace("5", "\u06F5") .Replace("6", "\u06F6") .Replace("7", "\u06F7") .Replace("8", "\u06F8") .Replace("9", "\u06F9"); } private static string toEnglishNumbers(string data) { if (string.IsNullOrWhiteSpace(data)) return string.Empty; return data.Replace("\u0660", "0") //٠ .Replace("\u06F0", "0") //۰ .Replace("\u0661", "1") //١ .Replace("\u06F1", "1") //۱ .Replace("\u0662", "2") //٢ .Replace("\u06F2", "2") //۲ .Replace("\u0663", "3") //٣ .Replace("\u06F3", "3") //۳ .Replace("\u0664", "4") //٤ .Replace("\u06F4", "4") //۴ .Replace("\u0665", "5") //٥ .Replace("\u06F5", "5") //۵ .Replace("\u0666", "6") //٦ .Replace("\u06F6", "6") //۶ .Replace("\u0667", "7") //٧ .Replace("\u06F7", "7") //۷ .Replace("\u0668", "8") //٨ .Replace("\u06F8", "8") //۸ .Replace("\u0669", "9") //٩ .Replace("\u06F9", "9"); //۹ }
جایگزین کردن حروف با استفاده از Replace معمولی توسط کاراکترها
اینبار همان حالت قبل را درنظر بگیرید؛ با این تفاوت که بجای رشتهها از کاراکترها استفاده شود. برای مثال بجای:
.Replace("\u0669", "9") //٩
.Replace('\u0669', '9') //٩
جایگزین کردن حروف با استفاده از String Builder
در ادامه بجای استفاده از متد Replace متداول، آرایهای از حروف قابل جایگزینی را توسط یک StringBuilder ایجاد کرده و حروف را یکی یکی تبدیل میکنیم و به این ترتیب برخلاف متد Replace، هربار برای جایگزینی یک مورد خاص، مجددا از ابتدای رشته شروع به جستجو نمیشود:
private static string toPersianNumbersUsingStringBuilder(string data) { if (string.IsNullOrWhiteSpace(data)) return string.Empty; var strBuilder = new StringBuilder(data); for (var i = 0; i < strBuilder.Length; i++) { switch (strBuilder[i]) { case '0': case '\u0660': strBuilder[i] = '\u06F0'; break; case '1': case '\u0661': strBuilder[i] = '\u06F1'; break; case '2': case '\u0662': strBuilder[i] = '\u06F2'; break; case '3': case '\u0663': strBuilder[i] = '\u06F3'; break; case '4': case '\u0664': strBuilder[i] = '\u06F4'; break; case '5': case '\u0665': strBuilder[i] = '\u06F5'; break; case '6': case '\u0666': strBuilder[i] = '\u06F6'; break; case '7': case '\u0667': strBuilder[i] = '\u06F7'; break; case '8': case '\u0668': strBuilder[i] = '\u06F8'; break; case '9': case '\u0669': strBuilder[i] = '\u06F9'; break; default: strBuilder[i] = strBuilder[i]; break; } } return strBuilder.ToString(); }
جایگزین کردن حروف با استفاده از ToCharArray
متد زیر دقیقا شبیه به حالت استفاده از String Builder است؛ با یک تفاوت مهم: بجای استفاده از String Builder برای تهیهی آرایهای از حروف قابل تغییر، از متد ToCharArray استفاده شدهاست:
private static string toPersianNumbersUsingToCharArray(string data) { if (string.IsNullOrWhiteSpace(data)) return string.Empty; var letters = data.ToCharArray(); for (var i = 0; i < letters.Length; i++) { switch (letters[i]) { case '0': case '\u0660': letters[i] = '\u06F0'; break; // مانند قبل } } return new string(letters); }
جایگزین کردن حروف با استفاده از string.Create
string.Create یکی از تازههای NET Core. است که امکان تغییر مستقیم یک قطعه string را میسر میکند:
private static string toPersianNumbersUsingStringCreate(string data) { if (string.IsNullOrWhiteSpace(data)) return string.Empty; return string.Create(data.Length, data, (chars, context) => { for (var i = 0; i < data.Length; i++) { switch (context[i]) { case '0': case '\u0660': chars[i] = '\u06F0'; break; // مانند قبل } } }); }
توضیحات بیشتر:
در دات نت، رشتهها نوعهای ارجاعی (reference type) غیرقابل تغییر (immutable) هستند. به این معنا که هر زمانیکه ایجاد شدند، دیگر نمیتوان محتوای آنها را تغییر داد. به همین جهت است که مجبور هستیم آنها را برای مثال توسط ToCharArray به یک آرایه تبدیل کنیم و سپس این آرایهی قابل تغییر را ویرایش کنیم. در حین کار با رشتهها، این غیرقابل تغییر بودن، سبب تخصیص حافظههای بیش از حدی میشوند. اگر بخواهیم قسمتی از یک رشته را جدا و یا جایگزین کنیم و یا تعدادی رشته را با هم جمع بزنیم، نتیجهی آن نیاز به یک تخصیص حافظهی جدید را دارد. راه حل استاندارد مواجه شدن با این مشکل، استفاده از StringBuilder است که از یک بافر داخلی برای انجام کارهای خودش استفاده میکند و زمانیکه نتیجهی نهایی را از آن درخواست میکنیم، تخصیص حافظهای را برای تولید رشتهی حاصل انجام میدهد. البته این مورد نیاز به اندازه گیری دارد و ارزش StringBuilder با حجم بالایی از اطلاعات متنی مشخص میشود؛ وگرنه همانطور که مشاهده میکنید (در نتیجهی نهایی بحث در ادامه)، الزاما کدهای سریعتری را به همراه نخواهد داشت.
هدف از string.Create، ایجاد رشتهها از دادههای موجود است. هدف اصلی آن کاهش تخصیصهای حافظه و کپی کردن اطلاعات است و امضای آن به صورت زیر میباشد:
public static string Create<TState> (int length, TState state, System.Buffers.SpanAction<char,TState> action);
هنگام کار با این متد، chars ای که در اختیار ما قرار میگیرد، یک <Span<char اشاره کننده به رشتهی نهایی است که قرار است بازگشت داده شود (در ابتدای کار بر اساس اندازهای که مشخص میشود، یک رشتهی خالی تخصیص داده میشود، اما بافر پر کردن آن اینبار در دسترس است و نیازی به تخصیص و کپی جداگانهای را ندارد). بنابراین روش کار با این متد، پر کردن بافر درونی رشتهی بازگشتی (همان chars در اینجا) به صورت مستقیم است؛ کاری که با یک رشتهی معمولی نمیتوان انجام داد.
State یا همان پارامتر دوم این متد، هر چیزی میتواند باشد. اگر نیاز است چندین رشته را در اینجا دریافت کنید تا بتوان بر اساس آن رشتهی نهایی را تشکیل داد، یک struct را تعریف کرده و بجای state به آن ارسال کنید. سپس این state توسط پارامتر context مربوط به SpanAction<char, string> action قابل دریافت و استفادهاست که در این مثال، context همان data ارسالی به این متد است.
سؤال: در حین کار با string.Create، باید از پارامتر data استفاده کنیم و یا از context دریافتی؟ به نظر در مثال فوق، data و context یکی هستند. اکنون داخل action delegate مهیا که جهت ساخت رشتهی نهایی بکار میرود، باید از data استفاده کرد و یا از context؟
return string.Create(data.Length, data, (chars, context) => {});
نتیجهی نهایی بررسی کارآیی روشهای مختلف جایگزین کردن حروف در یک رشته
کدهای کامل این مطلب را از اینجا میتوانید دریافت کنید: ReplacePerformanceTests.zip
ستون op/s در اینجا، مهمترین ستون گزارش است و به معنای تعداد عملیات قابل انجام در یک ثانیه است. از 670 هزار عملیات در ثانیه با Replace معمولی، به 5 میلیون عملیات در ثانیه رسیدهایم که بسیار قابل توجهاست.
همانطور که مشاهده میکنید، string.Create، سریعترین نگارش موجود است. در این بین نگارشی که از ToCharArray استفاده میکند، قابلیت انتقال بیشتری را دارد؛ از این جهت که نگارشهای دیگر NET. هنوز دسترسی به string.Create را ندارند. همچنین نگارش کاراکتری متد Replace، از متد رشتهای آن سریعتر عمل کردهاست.