Flutter 2.0: Enine Boyuna Null Safety

Flutter 2.0: Enine Boyuna Null Safety

Olmak ya da olmamak… İşte bütün mesele bu

Java gibi bir dilden geldiğinizi veya Flutter’ı bir süredir kullanıp kendinizi bir anda null safety hataları ile boğuşurken bulduğunuzu varsayarak başlayacağım. Bu nedenle, bu yazıda null safety ve hakkında kapsamlı bir açıklama yapmaya çalışmayacağım bunun yerine, null safety olarak bilinen boş işaretçi güvenliği temel fikrini anlatıp bunun Dart üzerinde ve Flutter’daki kullanımınız anlatmaya çalışacağım.

Null Safety Nedir?

Null hakkında açıklığa kavuşturmanız gereken iki nokta var. İlki ne olduğu, ikincisi kullanım alanı. Null hiç bir öğesi olmayan bir değişkene verilen isimdir. Temel olarak daha herhangi bir dataya sahip olmasa da bir türü ve bellekte bu türün kapladığı alan kadar alan kaplamaktadır.

Temelde kullanımını ise Java’dan örnek vermek istiyorum. Java’da tek bir tür olarak modelleyeceğiniz şeyi temsil etmek için iki ayrı türe sahip olma fikri vardır: null yapılabilir olanlar ve null yapılamayanlar. Örneğin, Java’da bir String nesnesine bir dize koyarsınız, dizenin boş olup olmadığına bakmadan bunun türünü String olarak atayacaktır. Ancak bu dize boş kalmaya devam ederse ilerde Java runtime bunu kullanmaya çalıştığımızda bir null pointer exception verecektir.

Örneğin şöyle bir şey yapacak olursak.

Burada temel olarak yaptığımız şey String ile işleyen bir fonksiyona Null girmek ki bu hata size bu programı çalıştırmak istediğinizde şöyle bir hataya mal olur.

Exception in thread "main" java.lang.NullPointerException
        at Main.checkNull(2021-04-22-dart1.java:8)
        at Main.main(2021-04-22-dart1.java:4)

Null safe bir dilde ise temelde iki türünüz vardır. Örneğin String için, String? **ve **String. İlki boş değerleri kabul eder (yukarıdaki Java örneğinde olduğu gibi) ancak ikincisi kabul etmez.

Böyle bir ayrıma gidilmesinin sebebi String veri türünü null alamaz hale getirmektir. Ve bu sayede burada girilecek veriyi kısıtlayabilirsiniz.

Ancak nullable bir veri türünde ise (String? veya Int? gibi) yapabileceğiniz konusunda birçok kısıtlama vardır: temelde bir boş değerle yapabileceğiniz şeylerle sınırlandırılırsınız, çünkü sonuçta değişken boş bir değer tutuyor olabilir. Bu tip değişkenleri kullanırken temel yaklaşım isNull ya da isEmpty benzeri bir yerleşik metod yardımı ile girilen değerin bir null mü yoksa dolu bir değişken mi olduğuna göre işlem yapmaktır.

Bu sayede, örneğin yukarda olduğu gibi, körü körüne bir değişken yöntemi çağırmadan ve bu hataları almadan yolumuza devam edebiliriz. Ancak bu tip dillerde bazı durumlarda değişken boş olsa bile bu ayrımlara girmeden çalıştırmak isteyebiliriz. Burada da karşımıza unsound Null Safety dediğimiz kavram çıkıyor.

Temelde bu sayede bir değişken null bile olsa, kodu çalıştıran ortam (Java için JVM sanal makinesi mesela) sessizce “başarısız” olacaktır. Ayrıca biz bunu yaparken yine bu tip null tipinde veriden kaynaklı hatalar baş gösterebilecek, ancak değişkenlerin kontrol edilmesinde değişken boş ise hiçbir şey olmayacaktır çünkü sessizmodda iken değişkenin davranışı için açıkça bir yönerge kodlamışızdır.

Dart Dilinde Null Safety

Baştan beri java java diyorum ama pek çok dil için null safety hala bulunmuyor. Bazı istisnalar dışında. Ve Dart da bu istisnaların artık birebir içerisinde. Dart 2.12 ile beraber, Dart da artık **null safe **yani boş değer güvenli bir dil oldu. (Bundan sonra boş değer diyeceğim)

Dart dili geliştiricileri bu değişimin dart için tarihindeki en büyk değişim olarak tanımlıyor. Üzerinde uğraştıkları pek çok konu ve diğer dillerin de bu konudaki izledikleri yolları iyice inceleyen ekip kendilerine göre bir boş değer güvenliği mekanizması oluşturdu. Dart dili geliştiricileri boş değer güvenliğini aşağıdaki 3 basit prensip dahilinde dilin temeline getirdi:

Bunlara prensip diyorum çünkü Dart’ın temelde boş değer güvenliği tamamiyle geliştiricinin keyfine bırakılmış gibi geliyor bana. Her ne kadar bazı kısıtlamaları getirse de hala yaya yaya, boş değer kontrollü yazmaya dikkat etmeden yazabilme imkanımız da yok değil

Dart’ta Boş Değer Güvenliği Öncesi ve Sonrası

Dart’ta 2.12’den önce değişken tanımlamasında şöyle bir yol benimsenmekte idi.

Burada gördüğümüz gibi Example sınıfı içerisinde bir değeri tanımlayıp, bu tanıma değer atanmasını bekletebiliyorduk. Ancak bu kodu dart 2.12 sonrasında derlemeye çalıştığımızda şu hatayı alacağız.

lib/main.dart:2:9:
Error: Field 'i' should be initialized because its type 'int' doesn't allow null.
     int i;
         ^ Error: Compilation failed.

Gördüğümüz gibi burada herhangi bir değer vermeden yaptığımız atama tamamı ile hataya sebep verdi.

Dart 2.12 sonrasında bir değişken kullanılmadan önce bir atama yapılmasını zorunlu kılmakta. Bu da şu demek. Bir değişkeni kullanmak için aynı blok içerisinde veri ataması yapmak gerekmekte.

İleride tabi bazı kolaylıklar göstereceğim. Şimdilik sadece kodunuzun nasıl geçersiz olduğunu göstermek için bir örnek verdim.

Şimdi dart’ın boş değer güvenliği özelliği ile bir proje açmayı veya mevcut bir dart projesini güvenli alana çekmeyi gösterelim.

Halihazırda Bulunan Bir Projede Boş Değer Güvenliğini Kullanmak

Dart Sürümümüzü Kontrol Edelim:

$ dart --version
Dart SDK version: 2.13.0-116.0.dev (dev) (Sun Mar 7 18:57:20 2021 -0800) on "linux_x64"

Şimdi de Boş Değer Kontrolü Aktif Mi Bakalım:

Kendi projelerimden birisinde aşağıdaki komut ile boş değer kontrolü durumunu kontrol ettim.

$ dart pub outdated --mode=null-safety

Sonuç temelde aşağıdaki gibi çıkacaktır.

Buradaki çıktıdan görebileceğimiz gibi, projenin eski bağımlılıkları boş değer kontrolüne uyumlu değilken bazı ileriki sürümleri sayesinde bu sorunu aşabiliriz. Bu temelde bağımlılıklarımızı getiren pubspec.yaml dosyasındaki bağımlılıkları analiz ederek yeni sürümlere yüseltmemizi sağlar. Bu çıktıdan görebileceğimiz kadarı ile basit bir paket yükseltmesi ile bu sorunu aşmak mümkün. Bunun için ise yapmamız gerek şu komut ile yeni bağımlılıkları pubspec.yaml içerisine yazdırıp bağımlılıkları proje içerisine çekmek olacak.

$ dart pub upgrade --null-safety
$ dart pub get

Bu komutları kullandıktan sonra Dart otomatik olarak pubspec.yaml’i değiştirmek için gerekli bağımlılıkları getirir.

**Önemli: **Bağımlılıklardan herhangi birisinin ileriki versiyonlarının boş değer güvenliğine uyumu yoksa bu kodunuzdan o kısımları tasfiye etmeden veya 3. parti kütüphanelerinizi geliştirenler buna çözüm bulmadan boş değer güvenliğini kullanamayacağınız anlamına gelir.

Kodları Göçürmek

Kulağa biraz korkutucu gelse de gerçeken de bu yapacağımız son adım. Dart diğer dillerin aksına boş değer güvenliğine geçtiği yolda geliştiriciyi yapayalnız bırakmadı. Geçişin düzgünce sağlanabilmesi amacıyla kendi göç aracını da dart derleyicine dahil etti. Bunu kullanmak için ise tek yapmamız gereken aşağıdaki komutu kullanmak

$ dart migrate

Bu %100 olarak kodlarınızın güvenle göçürüleceğini garanti etmeyen bir komut. Ama büyük oranda kodlarınızı sağlıklı bir şekilde göçürmenize yardım eder. Eğer ki kullandığınız 3. parti kütüphanelerin boş değer güvenliğine uygun hale gelmesi için kullanımı değişti ise (benim özelimde firebase ve tüm alt projelerinde bu değişim oldu) bu kısımları tek tek elle değiştirmek gerekir. Bu aşamada ise tam olarak şöyle bir hata alırsınız:

Eğer ki başarı ile bir dönüşüm yapıldı ise yapmanız gereken tek şey irili ufaklı bazı değişken sorunlarını gidermek ve çalışma esnasında yaşanabilecek bazı karmaşaları düzeltmek olacaktır.

Yeni Bir Projede Boş Değer Güvenliğini Kullanmak

Dart’ın son sürümünde yeni açılan bir projede öntanımlı olarak boş değer güvenliği açık olarak gelmekte. Ancak yine de öntanımlı olarak bu özellik açılması ise projeyi şu şekilde açmanız gerekebilir.

$ dart create -t console-full deneme
$ cd deneme
$ dart migrate --apply-changes

Biraz önce çalışmayan komut genelde yeni açılan projelerde alışır 😂

Boş Değer Güvenliği Dart’ı Nasıl Etkiledi?

Şimdi enine boyuna boş değer güvenliğinin Dart’a nasıl değişimler sağladığına bakalım.

Şuradan alındı: [link](https://dart.dev/null-safety/understanding-null-safety/hierarchy-before.png)

Boş değer kontrolü statik tip sistemde çözülmesi gereken bir problem ile başlar çünkü diğer her şey buna dayanır. Bir programlama dilinde eğer ki veriler null alabiliyorlarsa bu demektir ki programınız içerisindeki tüm türler için en az bir defa boş değer alabilme şansı verir. Boş değer güvenliği olmayan dillerde, statik tür sistemi, null değerinin bu türlerden herhangi birinin ifadelerine akmasına izin verir. Çünkü Null bütün veri türlerinin bir alt türü olarak görülür. Bilgisayar dillerinin pek çoğunun tür teorisine göre Null bütün veri türlerinin temelini veya hepsinin alt türünü oluşturur. Dart 2.12 öncesinde Dart için de bu durum böyleydi.

Bu tip bir statik tür sisteminde türe bağlı bazı fonksiyonların (örneğin List gibi gelişmiş türler için bazı getter, setter fonksiyonları boş değer alındığı için kullanılamaz olması ve durduk yere bir iç hata ile karşılaşılması hayli olası bir sonuçtur. Çünkü bu tir bir işlem boş değerini başka türden bir ifadeye akmasına izin vermeye çalışmaktır. Bu da yapacağımız işlemlerden herhangi birinin başarısız olabileceği anlamına gelir. Bu boş referans hatalarının özüdür — her hata, sahip olmadığı null üzerinde bir yöntem veya özelliği aramaya çalışmaktan kaynaklanır.

Şuradan alındı: [link](https://dart.dev/null-safety/understanding-null-safety/hierarchy-after.png)

Boş değer kontrollü olan dillerde ise hiçbir değer Null değeri alamaz şekilde temel türler boş değer türü olan Null den ayrılır.

Bu durumda Null artık bir alt tür olmadığından, özel Null sınıfı dışında hiçbir tür, null değerine izin vermez. Tüm türleri varsayılan olarak null yapılamaz hale gelmesi de kağıt üzerinde boş referans hatalarını düzeltecektir. Ancak, bu sefer de veri türü hiyerarşisinde boş değer alabilecek ara türler gerekecektir. Bu da bütün değişken ağacını boş değer alabilir ve alamaz türler olarak ikiye bölecektir.

Şuradan alındı: [link](https://dart.dev/null-safety/understanding-null-safety/bifurcate.png)

Türler ağacını bu şekilde null yapılabilir ve null yapılamaz olarak bölmek sağlamlığı ve çalışma zamanında hiçbir zaman boş referans hatası alamayacağınız ilkesini korurken, istediğiniz zaman da boş değeri referans vermek için hayli kullanışlı olmaktadır.

Örtülü boş değer bildirimlerinden kurtulmak ve Nullalt tür olarak kaldırmak, türlerin bir program boyunca atamalar arasında ve bağımsız değişkenlerden işlev çağrılarındaki parametrelere kadar bütün kullanımlarda boş değer hatasına bağlı anlaşılamaz bir sorunla karşılaşmaktan programcıyı kurtarır.

Boş Değer Güvenliği Kodunuzu Nasıl Etkileyecek?

Nullable Veri Türleriyle Tanışmak

Öncelikli olarak boş değeri elimizi kolumuzu sallayarak bir türe koyamayacağız. Örneğin:

Böyle bir bildirim bize şöyle bir hata çıktısı verecektir.

bin/my_cli.dart:2:17: Error: The value 'null' can't be assigned to a variable of type 'String' because 'String' is not nullable.
  String deneme=null;
                ^

Burada yapabileceğimiz bazı şeyler var. Öncelikle önceki listeyi hatırlayın. Eğer bir değişkene null vermek istiyorsanız nullable olan karşılığını kullanmanız gerekir. Örneğin:

Burda öğrenmemiz gereke ilk konu bu. String? ile gösterilen tür bizim boş referans değeri alabilen bir veri türümüz. Bu veri türü için iki ihtimal vardır. Ya değer olarak String **alır veya **null referansı alacaktır.

Programın çıktısı ise şu olacaktır.

null

Bir Nullable Veri Türünü Dönüştürmek

Null alabilen bir değişkenin içerisindeki veriyi almak için bize gereken şöyle bir yaklaşımdır.

Burada kodda gördüğümüz ! işareti null-aware işlemi olarak geçer. Temelde yaptığı ise String? veri türündeki değişkeni eğer null değilse String olarak içeriğini bize çevirmektir. Eğer değişken null ise şöyle bir hata alacağız.

Unhandled exception:
Null check operator used on a null value
#0      main (file:///home/zaryob/Development/my_cli /bin/my_cli.dart:3:32)
#1      _delayEntrypointInvocation.<anonymous closure> (dart:isolate-patch/isolate_patch.dart:281:32)
#2      _RawReceivePortImpl._handleMessage (dart:isolate-patch/isolate_patch.dart:184:12)

Bunun sebebi bu operatörün boş değer korumasının etki alanına değişkeni sokmaya çalışmasıdır. Bu sebeple değişken null olmasa bile kodu dart ile derlerken, derleyiciden şöyle bir uyarı alırız.

bin/my_cli.dart:3:26: Warning: Operand of null-aware operation '!' has type 'String' which excludes null.
  String deneme_icerik = deneme!;

Bu şu demek. Eğer ki hata alırsam dart mesul değildir. Böyle bir hata ile karşılaşmamak adına basit bir koşullu ifade kullanmamız yeterli gelecektir.

veya şöyle bir kullanışlı örnek de String özelinde işe yarayacaktır.

Bu tip kullanımlar bizim boş değer alabilecek bir değeri kullanabilir halde olmasını sağlamaktadır.

Sınıflarla Boş Değer Kullanımı Sonrası Yaşanan Değişimler

İşin içerisine sınıf girdiği zaman işler biraz karmaşıklaşabilir. Korkacak hiçbir şey yok. Eskiden Dart ile bir sınıf kullanacakken şöyle bir şey yapmamız yetiyordu.

late initialization

Temelde Javascript’deki late veya Kotlin’deki lazy gibi bir nesne tanımı Dart’ta da karşımıza çıkmaktadır.

Basit bir sınıf oluşturduk ve Deneme deneme; diyerek de deneme isminde bu sınıfa ait bir değişken oluşturduk. Ancak sınıf içerisindeki isim verisi içeriğini sonradan atadık. Sınıf oluşturucu fonksiyonu getirilene kadar isim tanımlaması yapmamak bazı durumlarda kullanışlı olmakta. Özellikle bu değeri dışarıdan girmek istediğimizde.

Normalde olsa bu kullanımdan yana hiç sorun olmayacak ama boş değer güvenliği yüzünden artık hata almaktayız.

lib/main.dart:7:10: Error: Field 'isim' should be initialized because its type 'String' doesn't allow null.
   String isim;
          ^^^^ Error: Compilation failed.

Boş değer güvenliği sonradan tanımlama yapılmayı engeller. Çünkü hiçbir kimse bu sonradan yapılacak tanımı garanti altına alamaz. Bu tip sonradan yapılacak tanımlamalar için late anahtar kelimesini kullanmamız gerekmektedir. Dart bunun sonradan tanımlanacağını anlayacak ve ona göre derleyecektir.

Ancak kodda bu değer sonradan tanımlanmadı ise yine hata oluşacaktır.

LateInitializationError: Field 'isim' has not been initialized.

required Parametreler

Null yapılamayan bir türe sahip bir parametreyi sınıf için kullanırken, bu parametrenin kesinlikle çağırılacağını garanti etmek ve boş değer asla görmememizi garanti etmek için, tür denetleyicisi tüm isteğe bağlı parametrelerin null yapılabilir bir türe veya varsayılan değere sahip olmasını gerektirir. Ya boş değer atanabilir tipte ve varsayılan değer içermeyen adlandırılmış bir parametreye sahip olmak istiyorsanız? Bu, bir parametrenin her zaman girilmesini zorunlu kılmak istediğiniz anlamına gelir. Başka bir deyişle, adlandırılmış ancak isteğe bağlı olmayan bir parametre kullanmak istediğiniz manasına gelmektedir. Bunu Dart’ta şu şekilde sağlarız:

abstract Sınıflar

Dart’ın zarif özelliklerinden biri, tek tip erişim ilkesi adı verilen bir şeyi desteklemesidir. Bazı Dart sınıfındaki bir “özelliğin” hesaplanıp hesaplanmadığı veya depolanıp depolanmadığını bazı durumlarda ayrıntılı bir şekilde modellememiz gerekmektedir. Modelleme işleminde bir iskelet oluşturur ardından bu iskelet için bir alan tanımlarız.

Modelleme yaparken soyut sınıflar yaparız. Soyut bir sınıf kullanarak bir arabirim tanımlarken, bir alan bildirimi kullanmamız gerekmektedir. Dart’ın eski zamanlarında bu tanımı şu şekilde yapmaktaydık:

Dart’ta artık iskelet olan sınıfları tam manası ile modelleme için kullanmak amacı ile iç değişkenler ve fonksiyonlara da abstract parametresi eklenmesi zorunlu hale getirildi. Bu sayede boş değer alınmasına bağlı sorunların önüne geçilmiş oldu:

Ayrıca bu tip bir belirtimin soyut metodlar için kullanılmasına gerek yoktur. Sadece fonksiyon türünü ve parametrelerini dinamik olacak şekilde belirtmek yeterli olacaktır. Yani aşağıdaki gibi:

Generic Tipler

Pek çok statik dil gibi, Dart da genel sınıflara ve genel yöntemlere sahiptir. Bu genel sınıflar ve genel yöntemler aynı soyut sınıflarda olduğu gibi bizi boş değer alıp alamayacağımızı düşünmeye itmektedir. Normalde generic tipler boş değer kontrolü olmadan şu şekilde tanımlanabilirken:

yeni gelen boş değer güvenliği sebebi ile şöyle bir tanımlama gerekmektedir.

Buradaki gibi objeyi bir boş değer alabilir hale getirdikten sonra bir unbox fonksiyonu kullanarak girilen verinin içeriğini boş değer hatası yaşamadan kolaylıkla almayı sağlayacaktır.

Özet

Boş değer güvenlik etrafında tüm dil ve kütüphane değişikliklerinde çok detaylı bir analiz istemekte. İşin içinde pek çok değişken durum ve davranışı etkileyen bir altyapı değişimi var. Bu da oldukça büyük bir dil değişikliğine sebep vermiştir. Bu, yalnızca tür sistemini değil, etrafındaki bir dizi diğer kullanılabilirlik özelliğini de değiştirmeyi gerektirmiş ve boş değer güvenliği özünde dilin imkanlarını kullanarak dili daha güvenli ve optimize hale getirmiştir.

Sonuç olarak, tüm bunları özümsedikten ve kodunuzu boş değer güvenlikli Dart dünyasına aldıktan sonra, derleyici kodunuzu oldukça güzel bir şekilde optimize edebilecek ve bir çalışma zamanı hatasının oluşabileceği her yeri size göstererek kodunuzu daha temiz ve güvenli hale getirebilirsiniz.

Kaynakça:

Void safety - Wikipedia Void safety (also known as null safety) is a guarantee within an object-oriented programming language that no object… Understanding null safety Null safety is the largest change we’ve made to Dart since we replaced the original unsound optional type system with a… Benefits of Null Safety I will assume that you come from Java, you already looked at some explanations somewhere else and did not fully… Sound null safety The Dart language now supports sound null safety! When you opt into null safety, types in your code are non-nullable by… Null safety in Flutter Flutter 2 supports null safety. You can migrate your Flutter packages to use non-nullable types like this: To learn… Migrating to null safety This page describes how and when to migrate your code to null safety. Here are the basic steps for migrating each