Tag Archives: .net

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.

.NET içerisinde Değer Tipi ve Referans Tipi

.NET kodunun çalıştırılmasını sağlayan CLR yapısı iki çeşit veri tipini destekler; referans tipi (reference types) ve değer tipi (value types). FCL (Framework Class Library) içerisinde bulunan tiplerin çoğu referans tipi olsa da, programcıların kullandığı tiplerin çoğunu değer tipleri oluşturur. Referans tiplerine her zaman belleğin “Heap” kısmından yer ayrılır ve C#’ın new operatörü de bu nesnenin bellek adresini döndürür (Bellek adresi de nesnenin bitlerini kaynak gösterir). Şunu aklınızda tutmalısınız ki, referans tipleri ile çalışırken bazı performans özelliklerine dikkat etmelisiniz. Şu 4 maddeyi göz önünde bulundurun:

1. Yönetilen heap kısmından bellek ayrılmak zorundadır.

2.Heap bölgesine ayrılmış her nesnenin kendisi ile ilişkili ilave üyeleri mevcuttur.

3.Nesnenin içineki diğer bitler her zaman sınıfa ayarlanır. (Alanlar için)

4.Bir nesneye heap bölgesinden kaynak ayırabilmek bazen Garbage Collection’ı ortaya çıkartabilir.

Her tip referans tipi olsaydı, bir uygulamanın performansı fazlasıyla kayba uğrardı. Düşünün ki, her Int32 tipini kullandığınızda bellekten bir yer tahsis etmek zorundasınız. Ne kadar kötü bir performans alacağınızı farketmişsinizdir. Performans arttırmak için, sıkça tip kullanılan uygulamalarda, CLR “hafif siklet” olarak adlandırdığı değer tiplerini önerir. Değer tipi örnekleri için çoğunlukla belleğin stack bölgesinden yer tahsis edilir (Ayrıca bir referans tipi nesnenin içerisine bir alan olarak gömülebilirler). Bu tipin bir değişkeni, örneğine bir pointer içermez, bu değişken örneğinin kendisinin bir alanını içerir. Değer tipi örnekleri, garbage collector’ın kontrolü altına girmezler ve bu sayede yönetilen heap kısmının üzerindeki yükü azaltır.

.NET Framework SDK dökümanı hangi tiplerin değer tipi, hangi tiplerin referans tipi olduğunu açık bir şekilde gösterir. Bu dökümana baktığımızda sınıf tipleri referans tipleridir. Örneğin; System.Exception, System.IO.FileStream ve System.Random birer referans tipleridir. Diğer taraftan döküman structure (yapı) ve enum tiplerinin değer tipi olduğunu gösterir. Örneğin; System.Int32 yapısı, System.DayOfWeek enum tipi, System.Decimal yapıları birer değer tipidir.

Biraz daha derinlere inmeye çalışırsak, tüm structure yapıların System.ValueType abstract tipinden türediğini görürüz. System.ValueType tipinin de System.Object tipinden türetildiğini görürürz. Tüm enum tipleri System.Enum abstract tipinden türetilmişlerdir ki bu tip te yine System.ValueType tipinden türetilir. CLR ve diğer tüm programlama dilleri enum tiplerine ayrı bir davranış sergilerler. Burada kafanıza şu soru takılabilir. Görüldüğü üzere System.ValueType bir sınıf tipidir ve sınıf tipleri referans tipidir. O halde Nasıl bir değer tipi bir referans tipinden türeyebilir?

Kendi değer tipinizi tanımlamaya çalışırken değer tiplerini temel tip olarak kullanamazsınız. Çünkü tüm değer tipleri sealed (yani o sınıf için türetilme işlemini önler) olarak tanımlıdır. Bu nedenle yeni oluşturmak istediğini referans tipinde ya da değer tipinde bir tip için, temel tip olarak değer tiplerini kullanamazsınız. Bir örnekle açıklamaya çalışırsam; Boolean, Char, Int32, Uint64, Single, Double tiplerini temel tip şeklinde kullanıp yeni bir tip oluşturmak imkansızdır.

Aşağıdaki örnek size referans tipi ve değer tiplerinin farklılığını açıklar:

using System;
namespace Program
{
 //Referans Tipi [Sınıf olduğundan]
 class ReferansTipi
 {
 public int x;
 }

//Değer Tipi [Struct olduğundan]
 struct DegerTipi
 {
 public int x;
 }

public class Program
 {
 public static void Main(string[] args)
 {
 ReferansTipi r1 = new ReferansTipi(); // Yer Tahsisi - Heap
 DegerTipi d1 = new DegerTipi(); // Yer Tahsisi - Stack

r1.x = 5; // Pointer referansı
 d1.x = 5; // Stack içerisinde değişim

Console.WriteLine(r1.x); // 5
 Console.WriteLine(d1.x); // 5
 }
 }
}

Bu kod parçasını çalıştırdığımızda bilgisayarımızın bellek kısmında aşağıdaki resimde görülen bir durum oluşur:

stack-heap

Harika(!) bir çizer olduğumu da bu resimden anlamışsınızdır. Şimdi aşağıdaki kodları yukarıda belirttiğimiz kodun içerisindeki Main() metodunun sonuna ekleyelim ve bellek kısmında nasıl bir değişiklik olduğuna bakalım:

ReferansTipi r2 = r1; // Sadece referansı (pointer) kopyalama

DegerTipi d2 = d1; // Yer Tahsisi - Stack ve üyeyi kopyalama

r1.x = 6; // r1.x ve r2.x değerlerini değiştirir.
d1.x = 7; // d1.x'i değiştirir, d2.x'i değiştirmez.

Console.WriteLine(r1.x); //6
Console.WriteLine(r2.x); //6
Console.WriteLine(d1.x); //7
Console.WriteLine(d2.x); //5

stack-heap1

Bu kod içerisinde bazı satırlara değinmek istiyorum:

DegerTipi d1 = new DegerTipi(); // Yer Tahsisi – Stack

Bu koda direkt olarak baktığımızda belleğin heap bölgesinden yer tahsis edildiği bilgisine varılır. Fakat, C# DegerTipi örneğinin bir değer tipi olduğunu bilir ve bununiçin stack bölgesinden yer ayıracak kodu üretir. C# ayrıca bu değer tipinin içerisindeki tüm alanların sıfıra set edildiğine emin olur. Aynı satır kod şu şekilde de yazılabilir;

DegerTipi d1;

Bu kod aynı şekilde thread kısmındaki stack bölgesinden yer ayırır ve tüm alanları sıfıraatar. Bu iki kod arasındaki tek fark, eğer new operatörünü kullanırsak C# düşünür ki bu örnek ilk kullanıma hazırdır. Hemen bu konuyu daha da açık hale getirelim;

DegerTipi d3 = new DegerTipi(); // Bu iki satır derlenir çünkü
int a = d3.x; // C#, d3'ün alanlarını sıfıra atandığını bilir.

DegerTipi d3; // Bu iki satır derlenimez çünkü C#, d3'ün alanlarını sıfıra
int a = d3.x; // atanmadığı düşünür. Use of possibly unassigned field 'd3' hatası alırız.

Eğer .NET içerisinde kendi değişken tipinizi tasarlamak isterseniz, bu tipin referans tipi mi yoksa değer tipi mi olacağına dikkatle karar vermelisiniz. Bazı durumlarda değer tipleri daha iyi performans verebilir. Eğer aşağıdaki 3 özellikte tasarlamak istediğiniz tip için uygunsa bu tipi değer tipi olarak tasarlamalısınız;

  1. Tip ilkel bir tip olursa (primitive types). Burada bahsetmek istediğim bu tipin içerisindeki alanlarda herhangi bir düzenlemesi gereken üyesi yoksa mantığıdır.
  2. Bu tipin diğer tiplerden kalıtım almasına gerek yoksa.
  3. Bu tipten türetilecek başka tipler yoksa.
    Yeni değişken tipinizin örnek boyutu da burada hesaba katılmalıdır. Çünkü varsayılan olarak, argümanlar metodlara değer ile geçirilirler (pass by value). Böylece değer tipi örneğinin içindeki alan kopyalanır ki bu da performansı azaltır. Tekrardan, geriye bir değer tipi döndüren metodlar, metodu çağıran tarafından bellekte örneğin içindeki alananın kopyası için yer tahsisi yaparlar ki bu da performansı azaltır.

Değer tiplerinin temel avantajı, bir nesne olarak belleğin yönetimli heap kısmında yer kaplamazlar. Tabi ki değer tiplerinin, referans tiplerine göre birkaç sınırlaması mevcuttur. Şimdi bu durumlara bir göz atalım;

1. Değer tipi nesnelerinin iki farklı gösterimi vardır. Boxed ve unboxed. Referans tipleri her zaman boxed formdadırlar.

2. Değer tipleri System.ValueType sınıfından türetilmişlerdir. Bu tip System.Object’te tanımlanan aynı metodları sağlar. Fakat, System.ValueType Equals() metodunu override eder. Böylece iki nesnedeki alanların içindeki değerler eşleşirse geriye true döndürür. Ek olarak, System.ValueType GetHashCode() metodunu da override eder ki bu method bir değer tipi için algoritma kullanarak bir hash kodu üretir.

3.Değer tiplerini temel sınıf (base class) olarak, herhangi bir değer tipi veya referans tipi üretemeyeceğimiz için, bir değer tipine yeni bir sanal (virtual) metod tanımlamamalıyız. Hiçbir metod soyut (abstract) olmamalıdır.

4. Referans tipi değişkenler belleğin heap kısmındaki nesnenin bellek adresini içerirler. Default olarak, referans tipinde bir değişken oluşturulduğunda, null’a atanır ki bunun anlamı bu değişken henüz geçerli bir nesneyi göstermiyordur. Null olarak atanmış bir referans nesnesini kullanmaya çalıştığımızda NullReferanceException hatası alırız. Buna karşı, değer tipi değişkenleri her zaman tanımlandığı tipin değerini içerir ve bu değer tipinin tüm üyeleri 0’a atanır. Değer tipi değişkeni bir referans içermediği için, bir değer tipine ulaşırken NullReferanceException hatası almamız imkansızdır.

5.Bir değer tipi değişkenini başka bir değer tipi değişkenine atadığımızda, alandan alana kopyalama gerçekleşir. Bir referans tipi değişkenini başka bir referans tipi değişkenine atadığımızda sadece bellek adresi kopyalanır.

6.Bir önceki maddeden dolayı, referans tipleri heap bölgesinde aynı nesneye referans olabileceklerinden, bir referans tipindeki değişkenin üzerindeki değişiklik diğer bir referans tipindeki değişkeni değiştirebilir. Diğer taraftan, bir değer tipindeki değişkenin diğer değer tipindeki değişkeni etkilemesi imkansızdır.

Değer tiplerinde Equals() metodunun varsayılan uygulaması olarak bit bit karşılaştırma yapması bazı durumlarda performansı yükseltebilir. Eğer iki adet değer tipi değişkeninizin içerisi pozitif sıfır ve negatif sıfır ise, sırasıyla eşit değil olarak karşılaştırılırlar. Ama Equals() metodunu varsayılan davranışı için override edebilirsiniz. Örneğin;

using System;

namespace Program
{
    public class Program
    {
        struct S
        {
            public double X;
        }

        public static void Main(string[] args)
        {
            var i = new S { X = 0.0 };
            var j = new S { X = -0.0 };

            Console.WriteLine(i.X.Equals(j.X)); // True
            Console.WriteLine(i.Equals(j)); // False
        }
    }
}