Tag Archives: .net

JIT Derleyicisine Giriş – Constant folding ve Function folding

.NET içerisinde Common Language Runtime’ın bir parçası olan JIT compiler‘ın sağladığı özelliklerden olan “constant folding” ve “function folding” kavramlarına baktık bu videoda.

Kaynaklar:

https://en.wikipedia.org/wiki/Constant_folding

https://en.wikipedia.org/wiki/Fold_(higher-order_function)

https://sharplab.io/

https://godbolt.org/

Minimal API | Route Handler için Response çeşitleri

Minimal API içerisinde bir route handler’dan döndürülebilecek response çeşitlerini ele aldık bu videoda.

✅Github reposu: https://github.com/sonergonul/MinimalAPI

Response çeşitleri şu şekilde;

➡️ IResult – Task<IResult> veya ValueTask<IResult>

➡️ string – Task<string> veya ValueTask<string> – text/plain

➡️ T – Json seralize – application/json

🟢 app.MapGet("/response1", () lambda "Hello World");

🟢 app.MapGet("/response2", () => new { Message = "Hello World" });

🟢 app.MapGet("/response3", () => Results.Ok(new { Message = "Hello World" }));

🟢 app.MapGet("/response4", () => Results.Json(new { Message = "Hello World" }));

🟢 app.MapGet("/405", () => Results.StatusCode(405));

🟢 app.MapGet("/text", () => Results.Text("This is some text"));

🟢 app.MapGet("/old-path", () => Results.Redirect("/new-path"));

🟢 app.MapGet("/download", () => Results.File("C:/appsettings.json", contentType: "application/json", fileDownloadName: "appsettings.json"));

JIT Derleyicisine Giriş – Dizilerde sınır kontrolü

.NET içerisinde Common Language Runtime‘ın bir parçası olan JIT compiler‘ın dizilerin index sınırları konusunda yaptığı optimizasyonlara baktık bu videoda.

JIT compiler’ı bir dizi içerisinde eğer aynı branch’te (yani farklı bir kod akışına dallanma olmadan) büyük index’li kontrolü yaptıktan sonra kendisinden küçük index’ler için ayrı bir kontrol yapmasına gerek olmayacak şekilde optimizasyon yapıyor.

Yani

arr[2] = 3;

için 2 index’i için bir karşılaştırma yaptığında, bu satırdan daha sonra aynı brach’te;

arr[1] = 3;

şeklinde bir kodunuz varsa, ben zaten 2 için kontrol ettim, 2 index’li bir elemanı olan bir dizinin 1 index’li bir elemanı her zaman vardır şeklinde düşünüp burada bir karşılaştırma yapmayarak optimizasyona gidiyor.

Tabi bu optimizasyon farklı branch’lerde mevcut değil.

Videodaki kod bloğu;

 public void M(int[] arr) 
    {
        arr[0] = 1;
        arr[1] = 2;
        arr[2] = 3;
    }
    
    public void M1(int[] arr) 
    {
        arr[2] = 1;
        arr[1] = 2;
        arr[0] = 3;
    }

    public void M(int[] arr, int i) 
    {
        if(i == 1)
        {
            arr[0] = 1;
            arr[1] = 1;
        }
        else if(i == 2)
        {
            arr[0] = 1;
        }
    }

Kaynaklar:

https://sharplab.io

https://github.com/dotnet/coreclr/blob/master/src/jit/rangecheck.cpp

https://en.wikipedia.org/wiki/Branch_predictor

JIT Derleyicisine Giriş – Tam sayı aritmetiği

.NET içerisinde Common Language Runtime‘ın bir parçası olan JIT compiler’ın tam sayı aritmetiği konusunda yaptıklarına değindim.

JIT derleme, interpreted programların performansını iyileştirmeye yönelik bir yöntem. Runtime sırasında, performansını artırmak için program assembly kod’ta derlenebilir. Buna sürece “dinamik derleme” de deniliyor.

Bu kavram ilk olarak John McCarthy’nin 1960 yılında yayınladığı ” Recursive functions of symbolic expressions and their computation by machine, Part I” çalışmasında belirtilmiş: http://jmc.stanford.edu/articles/recursive/recursive.pdf

Ayrıca şurada da “A Brief History of Just-In-Time” şeklinde güzel bir makale var: http://eecs.ucf.edu/~dcm/Teaching/COT4810-Spring2011/Literature/JustInTimeCompilation.pdf

JIT derlemenin statik derlemeye göre bazı avantajları vardır. C# uygulamalarını çalıştırırken, “runtime” uygulama çalıştırılırken uygulamanın profilini (profiling) çıkarabilir. Bu, daha optimize edilmiş kodun oluşturulmasına izin verir. Uygulama çalışırken davranışı değişirse, runtime bu kodu yeniden derleyebilir.

Dezavantajlardan bazıları, startup gecikmeleri ve runtime sırasında derleme ek yükünü barındırmak. Ek yükü sınırlamak için birçok jit derleyicisi yalnızca sık kullanılan kod kısımlarını derler mesela.

C# içerisinde bir kodun derlenme süreci şu şekilde;

1. Derleme, sırasında kaynak kodunuzu MSIL veya IL (tam özellikli bir dildir bu arada IL fakat C#’a göre biraz daha düşük seviyede) çevirir ve gerekli meta verileri oluşturur.

2. Runtime sırasında, JIT derleyicisi, aldığı IL kodunu assembly koduna çevirir. (Kullandığı runtime ve architecture’a göre üretilen assembly kodu da değişir. Örneğin; x86, x64 veya ARM gibi). Bu nedenle .NET Runtime assembly dosyalarınızın içerisindeki IL kodunu yorumlamaz.

3. CLR, kodun çalıştırılmasını sağlayan altyapıyı ve runtime sırasında kullanılabilecek hizmetleri sağlar. Derlenmiş makine kodu, kod bölümünün bir sonraki çalıştırılışında yeniden kullanılabilecek şekilde bellekte tutulur. Bir işlevi ikinci kez çağırdığınızda, onu ilk çağırdığınızdan daha hızlı çalışacaktır çünkü ikinci kez herhangi bir JIT adımı gerekli değildir.

Bunların hepsine genel olarak “managed execution” adı verilir.

Bu arada assembly kodları içerisinde sıklıkla görebileceğiniz bazı temel cpu register’ları şu şekilde;

EAX — Değerleri toplamak için kullanılan özel bir register

ECX — Counter (döngülerde ve string’lerde kullanılır)

EDX — EAX ile birlikte kullanılan, kısa süreli değişkenleri tutan register

EBX — Data depolamak için genel bir register. Dizilerde kullanılır.

Son olarak, JIT compiler açık kaynaktır. Şu Github sayfasından inceleme yapabilirsiniz:

https://github.com/dotnet/coreclr/tree/master/src/jit

Kaynaklar:

https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/operators/bitwise-and-shift-operators#right-shift-operator-

https://en.wikipedia.org/wiki/Logical_shift

https://en.wikipedia.org/wiki/Arithmetic_shift

https://en.wikipedia.org/wiki/X86_instruction_listings

https://en.wikipedia.org/wiki/Just-in-time_compilation

.NET 6 Yenilikleri – LINQ *OrDefault metodlarında default değerler

.NET 6 ile gelen yeniliklerden biri de LINQ içerisindeki *OrDefault metodlarında default değer atanma özelliği.

.NET 5 ve öncesinde IEnumerable içerisinde bulunan First, Last ve Single metodları aldıkları predicate’e göre herhangi bir eleman bulunamazsa InvalidOperationException fırlatıyorlardı.

Yani;

var list = new List int  { 1, 2, 3};

var first = list.First(i => i > 3); // InvalidOperationException
var last = list.Last(i => i > 3); // InvalidOperationException
var single = list.Single(i => i > 3); // InvalidOperationException

.NET 6 ile birlikte bu metodlara ek olarak FirstOrDefault, LastOrDefault ve SingleOrDefault metodlarına default değer atama özelliği geldi. Bunlar aldıkları predicate’e göre eğer bir eleman bulunamazsa defaultValue ile verdiğiniz değeri geri döndürüyorlar.

first = list.FirstOrDefault(i => i > 3, -1);
last = list.LastOrDefault(i => i > 3, -2);
single = list.SingleOrDefault(i => i > 3, -3);

Console.WriteLine($"{first} {last} {single}");
// -1 -2 -3

Kaynak: Announcing .NET 6 — The Fastest .NET Yet

https://devblogs.microsoft.com/dotnet/announcing-net-6/#system-linq-firstordefault-lastordefault-singleordefault-overloads-taking-default-parameters

.NET 6 Yenilikleri – Chunk ile collection’ları eşit parçalara ayırma

.NET 6 ile birlikte hayatımıza giren yeniliklerden biri de IEnumerable.Chunk metodu.

.NET 5 ve öncesinde, collection’ları eşit sayıda elemanlar olacak şekilde parçalara ayırmak için 2 yöntem vardı.

Birincisi aşağıdaki gibi bir custom (veya extension) metod yazmak;

static List List T Split T (IList T source, int size)
{
    return source.Select((x, i) = new { Index = i, Value = x })
        .GroupBy(x => x.Index / size)
        .Select(x => x.Select(v = v.Value).ToList())
        .ToList();
}

İkincisi de MoreLINQ içerisinde bulunan Batch metodunu kullanmaktı.

var buckets = numbers.Batch(10);

.NET 6 ile gelen IEnumerable.Chunk metodu ile bu işlemi artık custom bir metoda veya bir nuget paketine ihtiyaç duymadan halledebiliyoruz.

IEnumerable int numbers = Enumerable.Range(1, 34);
IEnumerable int[] buckets = numbers.Chunk(10);

foreach (int[] bucket in buckets)
{
    Console.WriteLine($"{bucket.First()} {bucket.Last()}");
}

Sonuç:

1 10
11 20
21 30
31 34

Kaynak: https://docs.microsoft.com/en-us/dotnet/api/system.linq.enumerable.chunk

.NET 6 Yenilikleri – Enumerable içerisinde *By metodları

.NET 6 ile gelen yeniliklerden biri de Enumerable içerisinde gelen *By extension metodları.

.NET 5 ve öncesi, tanımladığımız custom bir yapı için compare kısmını yaparken o yapıyı IComparable interface’inden türetip CompareTo metodunu kullanıyorduk klasik bir şekilde.

Örneğin;

record Product : IComparable
{
    public string Name { get; set; }
    public decimal Price { get; set; }
    public int CompareTo(object obj)
    {
        var person = (Product)obj;
        return decimal.Compare(Price, person.Price);
    }
}

Bu şekilde yaptığımız için karşılaştırma yapan metodları bu custom yapı için kullanabiliyorduk.

var products = new List of Product ();
products.Add(new Product { Name = "P1", Price = 15 });
products.Add(new Product { Name = "P2", Price = 5 });
products.Add(new Product { Name = "P3", Price = 25 });

var cheapest = products.Min();
Console.WriteLine(cheapest); // Product { Name = P2, Price = 5 }

var mostExpensive = products.Max();
Console.WriteLine(mostExpensive); // Product { Name = P3, Price = 25 }

Bu *By metodları ile, artık parametre verdiğimiz “keySelector” ile hangi özelliğe göre karşılaştırma yapılacağını belirtebiliyoruz. Bu nedenle bu custom yapıyı IComparable interface’inden türetmeye, doğal olarak ta CompareTo metodunu yazmamıza ihtiyacımız kalmıyor.

var cheapest = products.MinBy(p => p.Price);
var mostExpensive = products.Max(p => p.Price);

.NET 6 ile hayatımıza giren yeni metodlar şunlar; MaxBy, MinBy, DistinctBy, UnionBy, IntersectBy ve ExceptBy.

Kaynak: Announcing .NET 6 — The Fastest .NET Yet

https://devblogs.microsoft.com/dotnet/announcing-net-6/#system-linq-distinctby-unionby-intersectby-exceptby

.NET 6 Yenilikleri – ThrowIfNull ile null kontrolü

.NET 5 ve öncesinde, bir metod içerisinde argümanın null kontrolünü yapmak için kullandığımız klasik yöntem şu şekildeydi;

void MyMethod(object obj)
{
   if (obj is null)
   {
      throw new ArgumentNullException(nameof(obj));
   }
}

.NET 6 ile bunu daha kolay bir şekilde yapabilmemiz için statik bir ThrowIfNull metodu eklenmiş. İşlevsel olarak tamamen yukarıdaki kullanımla aynı mantıkta çalışıyor. Daha kısa ve sade bir kullanım.

void MyMethod(object obj)
{
   ArgumentNullException.ThrowIfNull(obj);
}

Kaynak: ArgumentNullException.ThrowIfNull(Object, String) Method

https://docs.microsoft.com/en-us/dotnet/api/system.argumentnullexception.throwifnull?view=net-6.0

.NET 6 Yenilikleri – DateOnly ve TimeOnly

Eğer .NET Framework ile aşinaysanız, DateTime ve DateTimeOffset gibi yapıları daha önce kullanmışsınızdır.

.NET 6 ile hayatımıza 2 yeni struct girdi: DateOnly ve TimeOnly.

DateOnly isimden de anlaşılacağı gibi “”sadece tarih” değerini barındırıyor. Bu yapı doğum günleri, işe alım tarihi, iş günleri gibi, belirli bir zaman ile ilişkisi olmayan birimler için gayet uygun bir yapı. Bu yapı tabi o tarih gününün “tamamını” kapsıyor şeklinde düşünebilirsiniz. Özellikle TimeZoneInfo gibi bir yapı içerisinde belki de o timezone için var olmayan bir saat değerine sahip bir DateTime’ı çevirirken oluşabilecek olası bug’ların da bu yapı sayesinde önüne geçiliyor.

DateTime gibi Kind özelliği yok DateOnly içerisinde, her zaman Unspecified olarak düşünebilrisiniz.

Sql Server vb. veritabanları ile iletişimde olduğunda da DateOnly yapısı gayet kullanışlı çünkü bir çok veritabanında var olan “date” yapısıyla gayet uyumlu. DateTime’ı bu tip ile eşleştirdiğinizde zaman özelliğini kaybediyordunuz, bu senaryoda en azından artık bir “zaman” olmadığını düşünebiliyoruz.

DateTime gibi 0001-01-01 ile 9999-12-31 aralığında. Ve yine DateTime gibi internal olarak gregoryan takviminde her zaman constructor içerisinde başka Calendar verseniz bile.

TimeOnly de isimden anlaşılacağı gibi, sadece günün saatini tutan bir struct. Bu yapı görüşme saati, günlük alarm saati gibi sadece saatin önemli olduğu kavramlar için gayet uygun.

TimeSpan gibi bir zaman aralığı değil de, gece yarısından itibaren geçen zaman’ı tutuyor bu. Aralığı 00:00:00.0000000 ile 23:59:59.9999999. DateTime gibi Ticks bazlı bir yapısı var (gece yarısından itibaren). TimeSpan gibi negatif olamıyor. TimeSpan’i günün saati olarak parse ederken sorunlar çıkabiliyordu, TimeOnly’de böyle bir risk yok.

Son olarak, TimeOnly’nin InBetween metodu çok güzel. Hem normal saat aralıkları için (10:00 ile 12:00 arasında mı), hem de gece yarısını geçen case’lerde (23:00 ile 02:00 arasında mı) çalışıyor.

Kaynaklar:

DateOnly MSDN: https://docs.microsoft.com/en-us/dotnet/api/system.dateonly?view=net-6.0

TimeOnly MSDN: https://docs.microsoft.com/en-us/dotnet/api/system.timeonly?view=net-6.0

Date, Time, and Time Zone Enhancements in .NET 6: https://devblogs.microsoft.com/dotnet/date-time-and-time-zone-enhancements-in-net-6/

DateTime.Parse(string) kullanmayı bırakamaz mıyız?

gandalf-parse

Scott Hanselman’ın What Great .NET Developers Ought To Know (More .NET Interview Questions) isimli yazısında da bahsedilği gibi, başta Stack Overflow olmak üzere birçok online sitede aşağıdaki şekilde kod örneklerine rastlıyorum:

DateTime dt = DateTime.Parse(myString);

Buna bir son verilmesi lazım. Bu kullanımda bir problem var. DateTime.Parse(string) metodu default olarak CurrentCulture ayalarını kullanmaktadır. Ve bu ayarlara istinaden direkt olarak bir string’i göndermek belirsiz durumlar oluşturmaya zemin hazırlar. Bu şu demek gibi;

Sana bir string gönderiyorum. CurrentCulture ayarlarına istinaden geçerli formatları sırasıyla dene, eğer biri uyuyorsa bu şekilde parse et. Eğer bu string için bu culture ayarlarında uygun bir format yoksa, bir FormatException fırlat.

Örnek olarak “01/02/2015” string’ini örnek alalım. Bu string hangi formatta? 1 Şubat 2015 mi? 2 Ocak 2015 mi? Cevap vermek imkansız zira bu sorunun cevabı dünyanın neresinde yaşadığınıza ve/veya hangi culture ayarlarını kullandığınıza göre değişir. Şehir/bölge olarak eşleştirilen Türkiye (“tr-TR”) ve Hindistan (“hi-IN”) için kullanılan culture ayarlarına ve bölgesel algıya göre bu string “1 Şubat 2015” olarak parse edilir fakat Amerika Birleşik Devletleri (“en-US”) için kullanılan culture ayarına göre “2 Ocak 2015” şeklinde parse edilir.

Bu DateTime.Parse(string) metodunun .NET Framework 4 versiyonu MSDN sayfasında (gördüğüm kadarıyla bu açıklama .NET Framework 4.5 ve 4.6 versiyonunda mevcut değil) da ifade edilir:

Because the Parse(String) method tries to parse the string representation of a date and time using the formatting rules of the current culture, trying to parse a particular string across different cultures can either fail or return different results. If a specific date and time format will be parsed across different locales, use the “DateTime.Parse(String, IFormatProvider)” method or one of the overloads of the ParseExact method and provide a format specifier.

Burada gördüğünüz gibi, spesifik bir format ile DateTime.ParseExact metodunu kullanmak çok daha kontrollü ve kesin bir sonuç döndürür. Tabi bu durumu gelen verinin formatını kesin olarak doğru bildiğimiz zamanlar kullanmalıyız. Eğer güvenmediğimiz veya bilmediğimiz bir yerden bu veriyi alıyorsak DateTime.TryParse veya DateTime.TryParseExact metodları daha uygun olur.

Açıklamada farklı sonuçlar oluşabileceği veya “fail” yani exception alabileceğimiz söyleniyor. Tabi ki bu çok normal. Zira “01/02:2015A22-22” gibi standart olmayan bir string gönderirsek, hangi culture ayarlarını kullanırsak kullanalım bir exception alırız ve bu exception’ı kod tarafıdan handle etmek zorunda kalırız. Standart olmayan veri dışında kullandığımız culture’ın DateTimeFormatInfo.Calendar özelliğinde var olan takvimin desteklediği sınırları aştığı durumlarda veya bu takvim üzerinde var olmayan bir tarihi parse etmeye çalıştığımızda da bu exception’ı alırız.

.NET Framework 4.5 versiyonunda çalıştırdığım şu kodu örnek alalım:

foreach (var culture in CultureInfo.GetCultures(CultureTypes.AllCultures))
{
    DateTime dt;
    if (!DateTime.TryParse("1500/12/29", culture, DateTimeStyles.None, out dt))
    {
        Console.WriteLine(culture.Name);
    }
}

Bu kod parçası normalde konsola harhangi bir şey yazdırmaz (eğer bu formatı desteklemeyen custom bir culture oluşturmadıysanız). Bu demek oluyor ki bu string formatı bir şekilde lokal makinanızdaki tüm culture ayarları tarafında parse edilebilir. Şimdi de string değerini “1500/12/30” olarak değiştirelim.

foreach (var culture in CultureInfo.GetCultures(CultureTypes.AllCultures))
{
    DateTime dt;
    if (!DateTime.TryParse("1500/12/30", culture, DateTimeStyles.None, out dt))
    {
        Console.WriteLine(culture.Name);
    }
}

Bu kod 6 adet sonuç üretiyor. TwoLetterISOLanguageName bakımından benzer olanları saymazsak bunlar “prs-AF” ve “yo-NG” değerleri. Peki bunların ortak özellikleri ne? Bu iki CultureInfo değeri DateTimeFormatInfo.Calendar olarak HijriCalendar kullanmakta. Ve bu takvimde 12. ay olan Zulhijjah (diğer adıyla Dhu al-Hijjah) sadece artık yıllarda 30 gün vardır. Diğer yıllarda 29 gün sürer bu ay. Bu arada HijriCalendar içerisinde Miladi takvime göre farklı bir artık yıl hesaplama kuralı vardır. Bu nedenle aslında bu takvimde var olmayan bir tarihi parse etmeye çalıştığımızdan exception almış oluruz. Son olarak string değerini “1500/12/31” olarak değiştirelim.

foreach (var culture in CultureInfo.GetCultures(CultureTypes.AllCultures))
{
    DateTime dt;
    if (!DateTime.TryParse("1500/12/31", culture, DateTimeStyles.None, out dt))
    {
        Console.WriteLine(culture.Name);
    }
}

Üstteki kod parçasında kullandığımız ”30” değeri için aldığımız sonuçlara ilave olarak “ar-SA” culture’ını da burada alırız. Neden? Çünkü bu culture değeri DateTimeFormatInfo.Calendar olarak UmAlQuraCalendar kullanmakta ve bu takvimin desteklediği maksimum tarih değeri “1500/12/30”. Bunu .NET Framework kodunu browser üzerinden gözlemleyebileceğiniz reference source içerisinde görebilirsiniz. Bu nedenle bu takvimin desteklediği tarih aralığı dışında bir tarih parse etmeye çalıştığımızdan exception almış oluruz. Aslında yukarıdakine benzer bir durum fakat yine de bahsetmek istedim.

    ////////////////////////////////////////////////////////////////////////////
    //
    //  Notes about UmAlQuraCalendar
    //
    ////////////////////////////////////////////////////////////////////////////
     /*
     **  Calendar support range:
     **      Calendar    Minimum     Maximum
     **      ==========  ==========  ==========
     **      Gregorian   1900/04/30   2077/11/17
     **      UmAlQura    1318/01/01   1500/12/30
     */

Bunların dışında DateTime.Parse(string) kullanımında, CurrentCulture konusunda kesin bir bilgi sahibi olunmasına kesin gözüyle bakamadığımızdan, “AM/PM” belirteçleri, tarih ayıracı, zaman ayıracı, gün isimleri, ay isimleri, kısaltılmış gün isimleri, kısaltılmış ay isimleri vb. özellikler culture bazlı farklılıklar gösterir. Örneğin; “Pazartesi” içeren bir string’i nasıl “en-US” culture’ı ile parse edemiyorsak, “Monday” içeren bir string’i de “tr-TR” culture’ı ile parse edemeyiz.

CurrentCulture ayarı konusunda kesin bilgimiz olsaydı bu herşeyi çözer miydi? Pek değil aslında. .NET Framework üzerindeki major ve minor değişiklikler veya işletim sistemi versiyonu değişikliklerinde bile CultureInfo özellikleri değişebiliyor. Bu konuda en önemli değişikliklerden biri it-IT culture’ı için .NET 3.5 versiyonundan 4.0 versiyonuna geçişte yaşandı. .NET 3.5 ve önceki versiyonlarda bu culture TimeSeparator olarak “.” kullanmasına rağmen .NET 4.0 versiyonu ile birlikte “:” kullanımına geçti. Date and time notation in Italy sayfasına göre de doğru olan “:” gibi görünüyor. Benzer durumlar Windows 10’a ilk geçiş sürecinde de yaşandı. Reddit’te benzer bir thread dahi mevcut. Ek olarak Culture data shouldn’t be considered stable (except for Invariant) yazısını da okuyabilirsiniz. Bu durum özet olarak CultureInfo sayfasında “Dynamic culture data” kısmında şu şekilde açıklanmış:

Except for the invariant culture, culture data is dynamic. This is true even for the predefined cultures. For example, countries or regions adopt new currencies, change their spellings of words, or change their preferred calendar, and culture definitions change to track this. Custom cultures are subject to change without notice, and any specific culture might be overridden by a custom replacement culture. Also, as discussed below, an individual user can override cultural preferences. Applications should always obtain culture data at run time.

When saving data, your application should use the invariant culture, a binary format, or a specific culture-independent format. Data saved according to the current values associated with a particular culture, other than the invariant culture, might become unreadable or might change in meaning if that culture changes.

Metinsel (string) bir veriyi Ticks bazlı bir veri yapısına (DateTime) parse etmenin karmaşıklığı yanı sıra, biz programcılar, kullandığımız culture ayarlarının bu gibi belirsiz değişimlere maruz kalabileceğini (InvariantCulture için bile olsa) unutmamalıyız. Bu nedenle nacizane fikrimce en iyi yaklaşım kendi static formatımızı oluşturmak veya değişimi düşük bir ihtimal olsa da InvariantCulture kullanmaktır. Yazdığımız bütün programların kendilerine özgü gereksinimleri olacaktır, bu yüzden bunları seçerken dikkkatli olmalıyız.