Bir istisna atıp atmamayı kontrol etmek için parametre ya da null değerini döndür - iyi uygulama?


24

Sıklıkla bir istisna atıldığını veya boşa döndürüldüğünü kontrol eden ek bir boolean parametresi olan yöntemler / işlevlerle karşılaşıyorum.

Bunlardan hangisinin hangi durumda daha iyi bir seçim olduğuna dair tartışmalar var, bu yüzden buna odaklanmayalım. Bakınız örneğin , Büyü değeri döndür, istisna at veya başarısızlık durumunda false döndür?

Bunun yerine her iki yolu da desteklemek istediğimiz için iyi bir neden olduğunu varsayalım.

Şahsen böyle bir yöntemin ikiye ayrılması gerektiğini düşünüyorum: Biri başarısızlığa istisna yapan, diğeri başarısızlığa boşa dönen.

Peki, hangisi daha iyi?

A: $exception_on_failureParametreli bir yöntem .

/**
 * @param int $id
 * @param bool $exception_on_failure
 *
 * @return Item|null
 *   The item, or null if not found and $exception_on_failure is false.
 * @throws NoSuchItemException
 *   Thrown if item not found, and $exception_on_failure is true.
 */
function loadItem(int $id, bool $exception_on_failure): ?Item;

B: İki farklı yöntem.

/**
 * @param int $id
 *
 * @return Item|null
 *   The item, or null if not found.
 */
function loadItemOrNull(int $id): ?Item;

/**
 * @param int $id
 *
 * @return Item
 *   The item, if found (exception otherwise).
 *
 * @throws NoSuchItemException
 *   Thrown if item not found.
 */
function loadItem(int $id): Item;

EDIT: C: Başka bir şey?

Birçok insan başka seçenekler önerdi veya hem A hem de B'nin kusurlu olduğunu iddia etti. Bu tür öneriler veya görüşler memnuniyetle karşılanır, alakalı ve faydalıdır. Tam bir cevap, bu tür ekstra bilgiler içerebilir, ancak aynı zamanda imza / davranışı değiştirecek bir parametrenin iyi bir fikir olup olmadığı konusundaki ana soruyu da ele alacaktır.

notlar

Birisinin merak etmesi durumunda: Örnekler PHP'dedir. Ancak, sorunun PHP veya Java ile aynı olduğu sürece diller için geçerli olduğunu düşünüyorum.


5
PHP'de çalışmam gerekecek ve oldukça da yetkin olmama rağmen, sadece kötü bir dil ve standart kütüphanesi her yerde. Referans için eev.ee/blog/2012/04/09/php-a-fractal-of-bad-design adresini okuyun . Bu yüzden bazı PHP geliştiricilerin kötü alışkanlıkları benimsemeleri şaşırtıcı değil. Ancak bu özel tasarım kokusunu henüz görmedim.
Polygnome

Verilen cevaplarda önemli bir noktayı özlüyorum: ne zaman diğerini kullanacaksınız. İstisna yöntemini, bulunamayan bir öğenin beklenmedik olması ve bir hata olarak kabul edilmesi durumunda (panikleme zamanı) kullanmanız gerekir. Eksik bir öğenin muhtemel olmaması ve kesinlikle bir hata olmaması durumunda, sadece normal akış kontrolünüze dahil etme ihtimaliniz varsa Try ... yöntemini kullanırsınız.
Martin Maat


1
@Polygnome Haklısın, standart kütüphane fonksiyonları her yerde, ve çok sayıda zayıf kod var ve talep başına bir işlem sorunu var. Ancak her sürümde daha iyi oluyor. Türler ve nesne modeli, ondan bekleyebileceği şeylerin tümünü veya çoğunu yapar. Jenerik, eş ve çelişki, imza belirten geri arama türlerini, vb. Görmek istiyorum. Bunların çoğu tartışılıyor veya uygulanıyor. Bence genel kütüphane iyi, standart kütüphane meraklıları kolayca geçmeyecek olsa bile.
donquixote

4
Bir öneri: Bunun yerine, sizin için anlamlı loadItemOrNull(id)olup olmadığını düşünün loadItemOr(id, defaultItem). Öğe bir String veya sayı ise, genellikle yapar.
user949300

Yanıtlar:


35

Haklısınız: iki nedenden ötürü, birkaç nedenden dolayı daha iyidir:

  1. Java'da, potansiyel olarak bir istisna atan yöntemin imzası, bu istisnayı; diğer yöntem olmaz. Özellikle birinden ve diğerinden ne bekleyeceğini açıkça ortaya koyuyor.

  2. Yöntemin imzasının istisnalar hakkında hiçbir şey söylemediği C # gibi dillerde, genel yöntemler hala belgelenmelidir ve bu belgeler istisnaları içerir. Tek bir yöntemi belgelemek kolay olmaz.

    Örneğiniz mükemmel: ikinci kod parçasındaki yorumlar çok daha net görünüyor ve ben de “Öğe bulunursa (aksi takdirde istisna).”, “Öğe” ye kadar - potansiyel bir istisna ve varlığı Ona verdiğin açıklama kendini açıklar.

  3. Tek bir yöntem durumunda, çalışma zamanında boole parametresinin değerini değiştirmek istediğiniz birkaç durum vardır ve eğer yaparsanız, arayan kişinin her iki vakayı da ele alması gerektiği anlamına gelir (bir nullyanıt ve istisna) ), kodu olması gerekenden çok daha zorlaştırıyor. Seçim çalışma zamanında değil, kodu yazarken iki yöntem kusursuz olur.

  4. .NET Framework gibi bazı çerçeveler bu durum için sözleşmeler oluşturmuştur ve sizin önerdiğiniz gibi iki yöntemle çözerler. Tek fark, cevaplarında Ewan tarafından açıklanan bir kalıp kullanmalarıdır , bu yüzden int.Parse(string): intbir istisna int.TryParse(string, out int): boolalamaz. Bu adlandırma kuralı, .NET'in topluluğunda çok güçlüdür ve kod, tanımladığınız durumla eşleştiğinde izlenmelidir.


1
Teşekkürler, aradığım cevap buydu! Amaç Ben Drupal CMS, bu konu hakkında bir tartışma başladı oldu drupal.org/project/drupal/issues/2932772 ve bu benim kişisel tercihi hakkında olabilir, ama diğerlerinden geribildirim ile yedeklemek istemiyoruz.
donquixote

4
Aslında, en azından. NET'in uzun süredir devam eden geleneği, iki yöntem kullanmamak, bunun yerine doğru iş için doğru seçimi seçmek. İstisnalar, istisnai durumlar için, beklenen kontrol akışını ele almamaktır. Bir dizgiyi bir tamsayıya ayrıştıramadı Beklenmeyen veya istisnai bir durum değil. int.Parse()gerçekten kötü bir tasarım kararı olarak kabul edilir, bu yüzden .NET ekibi devam edip uygulamıştır TryParse.
Voo

1
Ne tür bir kullanım olayıyla ilgilendiğimizi düşünmek yerine iki yöntem sunmak kolay yoludur: Ne yaptığınızı düşünmeyin, bunun yerine API'nizin tüm kullanıcılarına sorumluluk verin. Tabii çalıştığını, ama bu iyi API tasarımının özelliğidir (veya konuyla herhangi bir tasarım değil Okuma örn. Joel'in take bu konuda .
Voo

@Voo Geçerli nokta. Nitekim iki yöntem sağlayarak arayanın seçimini yapar. Belki de bazı parametre değerleri için bir öğenin mevcut olması beklenirken, diğer parametre değerleri için mevcut olmayan bir öğe normal bir durum olarak kabul edilir. Belki de, bileşenlerin her zaman bulunması beklenen bir uygulamada ve var olmayan öğelerin normal olarak değerlendirildiği başka bir uygulamada kullanılır. Belki arayan, aramadan ->itemExists($id)önce arar ->loadItem($id). Belki de bu, geçmişte diğer insanlar tarafından yapılmış bir seçimdir ve bunu onaylamak için almalıyız.
donquixote

1
Sebep 1b: Bazı diller (Haskell, Swift) imzada yer almayı gerektirir. Spesifik olarak, Haskell null değerleri ( data Maybe a = Just a | Nothing) için tamamen farklı bir tipe sahiptir .
John Dvorak

9

İlginç bir varyasyon Swift dilidir. Swift'de, bir işlevi "atma" olarak ilan edersiniz, yani hata atmasına izin verilir (benzer ancak Java istisnaları ile aynı değildir). Arayan bu işlevi dört şekilde çağırabilir:

a. Bir try / catch ifadesinde istisnayı yakalamak ve ele almak.

b. Arayanın kendisi atma olarak bildirilirse, sadece onu arayın ve atılan herhangi bir hata arayan tarafından atılan bir hataya dönüşür.

c. İşlev çağrısının işaretlenmesi, try!"Bu aramanın atmayacağından eminim" anlamına gelir. Denilen işlevi ise yaptığı atışı, uygulama kazasında garantilidir.

d. İşlev çağrısının işaretlenmesi, try?"İşlevin atabileceğini biliyorum ama hangi hatanın tam olarak atıldığını umursamıyorum" anlamına gelir. Bu, fonksiyonun dönüş değerini " T" türünden " optional<T>" türüne değiştirir ve atılan bir istisna döndüren sıfır değerine dönüşür.

(a) ve (d) sorduğunuz davalar. İşlev sıfır döndürürse ve istisnalar atabilirse ne olur ? Bu durumda, geri dönüş değerinin optional<R>türü bazı R türleri için " " olur ve (d) bunu optional<optional<R>>biraz " " olarak değiştirir ve bu biraz kriptik ve anlaşılması zor ancak tamamdır. Ve bir Swift işlevi geri dönebilir optional<Void>, bu nedenle (d) Void işlevini döndüren işlevleri atmak için kullanılabilir.


2

Ben bool TryMethodName(out returnValue)yaklaşımı severim .

Size yöntemin başarılı olup olmadığına dair kesin bir gösterge verir ve adlandırma bir istisna atma yöntemini bir try catch bloğunda kaplayan eşleşir.

Sadece null değerini döndürürseniz, başarısız olup olmadığını veya dönüş değerinin yasal olarak null olduğunu bilmiyorsunuz.

Örneğin:

//throws exceptions when error occurs
Item LoadItem(int id);

//returns false when error occurs
bool TryLoadItem(int id, out Item item)

örnek kullanım (çıkış, başlatılmamış referans ile geçmek anlamına gelir)

Item i;
if(TryLoadItem(3, i))
{
    Print(i.Name);
}
else
{
    Print("unable to load item :(");
}

Üzgünüm, beni burada kaybettiniz. " bool TryMethodName(out returnValue)Yaklaşımınız" orijinal örnekte nasıl görünür?
donquixote

bir örnek eklemek için düzenlendi
Ewan

1
Yani bir dönüş değeri yerine bir yan referans parametresi kullanırsınız, bu nedenle geri dönüş değeri için bool başarısı serbest olur mu? Bu "out" anahtar kelimesini destekleyen bir programlama dili var mı?
donquixote

2
Evet, üzgünüm 'dışarı' c # özgüdür fark
Ewan

1
her zaman hayır Ama madde 3 ya eğer olduğunu boş? bir hata olup olmadığını bilemezsiniz
Ewan

2

Genellikle bir boolean parametresi bir işlevin ikiye bölünebileceğini gösterir ve bir kural olarak, bir booleandan geçmek yerine her zaman onu bölmelisiniz.


1
Bazı vasıflar veya nüanslar eklemeden bunu bir kural olarak dışlamazdım. Bir boole ifadesini argüman olarak kaldırmak, eğer başka bir yerde / başka bir yerde bir aptal yazmaya zorlayabilir ve böylece değişkenleri değil yazılı kodunuzdaki verileri kodlarsınız.
Gerard

@Gerard bana bir örnek verebilir misiniz lütfen?
Maksimum

@Max Bir boolean değişkeni olduğunda b: Arama yapmak yerine , diğer argümanları veya dilin üçlü bir işleci ve birinci sınıf işlevleri / yöntemleri varsa böyle bir şeyi foo(…, b);yazıp if (b) then foo_x(…) else foo_y(…);tekrarlamanız gerekir ((b) ? foo_x : foo_y)(…). Ve bu belki programın birkaç farklı yerinden tekrarlandı.
BlackJack

@ BlackJack iyi evet sizinle aynı fikirdeyim. Örneğimi düşündüm, if (myGrandmaMadeCookies) eatThem() else makeFood()ki evet , bunun gibi başka bir işlevi de sarabilirim checkIfShouldCook(myGrandmaMadeCookies). Bahsettiğiniz nüansın, asıl soruda olduğu gibi, işlevin nasıl davranmasını istediğimiz konusunda bir boole geçip geçmediğimize dair bir ilgisi olup olmadığını merak ediyorum. Büyükannem örneği verdim.
Maksimum

1
Benim yorumumda belirtilen nüans "her zaman gerekir" anlamına gelir. Belki de "a, b ve c uygulanabilirse [...]" olarak tekrarlayın. Bahsettiğiniz "kural" harika bir paradigma, ancak kullanım durumunda çok değişken.
Gerard

1

Temiz Kod , boolean flag argümanlarından kaçınmanızı önerir, çünkü anlamları çağrı alanında opaktır:

loadItem(id, true) // does this throw or not? We need to check the docs ...

ayrı yöntemler ifade isimlerini kullanabilirken:

loadItemOrNull(id); // meaning is obvious; no need to refer to docs

1

Ars veya Mourzenkos'un cevabında belirtilen nedenlerden dolayı A veya B'yi kullanmam gerekirse, B'yi iki ayrı yöntem olarak kullanırdım .

Ancak başka bir yöntem var 1 : Java'da buna denir Optional, ancak aynı kavramı, dil zaten benzer bir sınıf sunmuyorsa, kendi türlerinizi tanımlayabileceğiniz diğer dillere de uygulayabilirsiniz.

Yönteminizi şu şekilde tanımladınız:

Optional<Item> loadItem(int id);

Metodunuzda Optional.of(item)eğer öğeyi bulursanız geri dönersiniz ya da bulmuyorsanız geri dönersiniz Optional.empty(). Asla 2 atmaz ve asla geri dönemezsin null.

Metodunuzun müvekkiline göre, bunun gibi bir şey null, ancak büyük bir farkla, eksik maddelerin durumu hakkında düşünmeye zorlaması. Kullanıcı, sonuç olamayacağı gerçeğini görmezden gelemez. Öğeyi Optionalaçık bir işlemden çıkarmak için :

  • loadItem(1).get()atar NoSuchElementExceptionveya bulunan öğeyi döndürür.
  • loadItem(1).orElseThrow(() -> new MyException("No item 1"))Dönen Optionalboşsa özel bir istisna kullanmak da mümkündür .
  • loadItem(1).orElse(defaultItem)Bir istisna atmadan , bulunan öğeyi veya geçenleri defaultItem(aynı zamanda olabilir null) döndürür .
  • loadItem(1).map(Item::getName)Optionaleğer varsa, yine öğeler ismiyle birlikte geri dönecektir .
  • Bazı başka yöntemler var, Javadoc'aOptional bakınız .

Bunun avantajı, artık ürün kodu yoksa müşteri koduna kalmış olması ve hala tek bir yöntem sağlamanız gerektiğidir .


1 Bunun gibi bir şey olduğunu tahmin try?de gnasher729s cevap ama Swift bilmiyorum olarak tamamen bana belli değil ve bunun belirli Swift için yani bir dil özelliği gibi görünüyor.

2 Başka nedenlerle istisnalar atabilirsiniz, örneğin bir öğenin olup olmadığını bilmiyorsanız, veritabanıyla iletişim kurmakta zorlandığınız için.


0

İki yöntem kullanmaya karar vermekte olan bir başka nokta olarak, bunu uygulamak çok kolay olabilir. PHP'nin sözdizimini bilmediğim için örneğimi C # ile yazacağım.

public Widget GetWidgetOrNull(int id) {
    // All the lines to get your item, returning null if not found
}

public Widget GetWidget(int id) {
    var widget = GetWidgetOrNull(id);

    if (widget == null) {
        throw new WidgetNotFoundException();
    }

    return widget;
}

0

İki yöntemi tercih ediyorum, bu şekilde, bana sadece bir değer döndürdüğünüzü değil, tam olarak hangi değeri verdiğinizi söyleyeceksiniz.

public int GetValue(); // If you can't get the value, you'll throw me an exception
public int GetValueOrDefault(); // If you can't get the value, you'll return me 0, since it's the default for ints

TryDoSomething () ya da kötü bir kalıp değildir.

public bool TryParse(string value, out int parsedValue); // If you return me false, I won't bother checking parsedValue.

Daha önce veri tabanı etkileşimlerinde eskileri görmeye alışkınım, ikincisi daha çok ayrıştırma işlemlerinde kullanılır.

Diğerlerinin de söylediği gibi, bayrak parametreleri genellikle bir kod kokusudur (kod kokuları, yanlış yaptığınız anlamına gelmez, yalnızca yanlış yapma olasılığınız vardır). Tam olarak isimlendirmene rağmen, bazen bu bayrağı nasıl kullanacağını bilmeyi zorlaştıran nüanslar olabilir ve sonunda yöntem gövdesinin içine bakarsın.


-1

Diğer insanlar aksini önermekle birlikte, iki kullanım senaryosu için hataların nasıl ele alınması gerektiğini belirten bir parametreye sahip bir yönteme sahip olmanın daha iyi olduğunu belirtmek isterim. Aynı zamanda "hata atma" ile "hata iadesi" hata gösterimi "kullanımları için ayrı giriş noktalarına sahip olmak istenebilse de , her iki kullanım modeline de hizmet edebilecek bir giriş noktasına sahip olmak, sarma yöntemlerinde kod çoğaltılmasını önleyecektir. Basit bir örnek olarak, eğer birinin işlevi varsa:

Thing getThing(string x);
Thing tryGetThing (string x);

ve biri aynı şekilde davranmak istiyor, ancak x'in köşeli parantez içinde olmasıyla iki sarıcı işlev seti yazmanız gerekecek:

Thing getThingWithBrackets(string x)
{
  getThing("["+x+"]");
}
Thing tryGetThingWithBrackets (string x);
{
  return tryGetThing("["+x+"]");
}

Hataların nasıl ele alınacağına dair bir tartışma varsa, yalnızca bir sarmalayıcıya ihtiyaç duyulur:

Thing getThingWithBrackets(string x, bool returnNullIfFailure)
{
  return getThing("["+x+"]", returnNullIfFailure);
}

Paketleyici yeterince basitse, kod çoğaltma çok kötü olmayabilir, ancak herhangi bir şekilde değiştirilmesi gerekebilirse, kodun çoğaltılması için değişikliklerin çoğaltılması gerekir. Biri ekstra argümanı kullanmayan giriş noktaları eklemeyi seçse bile:

Thing getThingWithBrackets(string x)
{
   return getThingWithBrackets(x, false);
}
Thing tryGetThingWithBrackets(string x)
{
   return tryGetThingWithBrackets(x, true);
}

biri tüm "iş mantığını" tek bir yöntemde tutabilir - başarısızlık durumunda ne yapılması gerektiğini belirten bir argümanı kabul eden.


Haklısınız: Dekoratörler ve sarmalayıcılar ve hatta düzenli uygulamalarda, iki yöntem kullanılırken bazı kod çoğaltmaları olur.
17'de donquixote

İstisnai olarak ayrı paketlere atılmaksızın ve olmadan sürümleri koymak ve sarmalayıcıları genel yapmak?
Crawford,

-1

Bir istisnanın atılıp atılmadığını kontrol eden bir bayrağa sahip olmamanız gerektiğini açıklayan tüm cevapları düşünüyorum. Onların tavsiyesi, bu soruyu sorma ihtiyacı duyan herkese tavsiyem olur.

Bununla birlikte, bu kural için anlamlı bir istisna olduğunu belirtmek istiyorum. Yüzlerce başka insanın kullanacağı API'ler geliştirmeye başlamak için yeterince gelişmiş olduğunuzda, böyle bir bayrak vermek isteyebileceğiniz durumlar vardır. Bir API yazarken, sadece saflara yazmazsın. Gerçek arzuları olan gerçek müşterilere yazıyorsun. İşinizin bir parçası, API'nızla onları mutlu etmektir. Bu, bir programlama probleminden ziyade sosyal bir problem haline gelir ve bazen sosyal problemler, kendi başına programlama çözümleri kadar ideal olan çözümleri belirler.

API'nizde biri istisnalar diğeri istemeyen iki ayrı kullanıcınız olduğunu görebilirsiniz. Bunun gerçekleşebileceği gerçek hayattan bir örnek, hem geliştirilmekte hem de gömülü bir sistemde kullanılan bir kütüphanedir. Geliştirme ortamlarında, kullanıcılar muhtemelen her yerde istisnalar olmak isteyecektir. İstisnalar, beklenmeyen durumları ele almak için çok popülerdir. Ancak, birçok yerleşik durumda yasaklanmıştır, çünkü etrafındaki gerçek zamanlı kısıtlamaları analiz etmek çok zordur. Aslında sadece işlevinizi yerine getirmek için gereken ortalama süreyi değil, aynı zamanda herhangi bir bireysel eğlence için geçen süreyi umursuyorsanız , yığını herhangi bir yerde serbest bırakma fikri çok istenmeyen bir durumdur.

Benim için gerçek bir yaşam örneği: bir matematik kütüphanesi. Eğer matematik kütüphanenizde Vectoraynı yöne birim vektör almayı destekleyen bir sınıf varsa, büyüklük ile bölmeniz gerekir. Büyüklüğü 0 ise, sıfır durumuna göre bir bölme var. Gelişmede bu yakalamak istiyorum . Gerçekten de 0'lı takılarak şaşırtıcı bir şekilde ayrılmak istemiyorsunuz. Bir istisna atmak bunun için çok popüler bir çözümdür. Neredeyse hiçbir zaman olmaz (gerçekten istisnai bir davranıştır), ancak olduğu zaman bunu bilmek istersiniz.

Gömülü platformda bu istisnaları istemezsiniz. Çek yapmak daha mantıklı olur if (this->mag() == 0) return Vector(0, 0, 0);. Aslında, bunu gerçek kodda görüyorum.

Şimdi bir iş açısından düşünün. Bir API kullanmanın iki farklı yolunu öğretmeye çalışabilirsiniz:

// Development version - exceptions        // Embeded version - return 0s
Vector right = forward.cross(up);          Vector right = forward.cross(up);
Vector localUp = right.cross(forward);     Vector localUp = right.cross(forward);
Vector forwardHat = forward.unit();        Vector forwardHat = forward.tryUnit();
Vector rightHat = right.unit();            Vector rightHat = right.tryUnit();
Vector localUpHat = localUp.unit()         Vector localUpHat = localUp.tryUnit();

Bu, buradaki cevapların çoğunun görüşlerini tatmin eder, ancak şirket açısından bu istenmeyen bir durumdur. Gömülü geliştiriciler bir kodlama stili öğrenmek zorunda kalacak ve geliştirme geliştiricileri başka bir tane öğrenecek. Bu oldukça sinir bozucu olabilir ve insanları bir şekilde düşünmeye zorlar. Potansiyel olarak daha kötü: Gömülü sürümün herhangi bir istisna atma kodu içermediğini nasıl kanıtlarsınız? Fırlatma sürümü kolayca karışabilir ve pahalı bir Arıza İnceleme Kurulu bulana kadar kodunuzda bir yere gizlenebilir.

Öte yandan, istisna işlemeyi açan veya kapatan bir bayrağa sahipseniz, her iki grup da aynı kodlama stilini öğrenebilir. Aslında, çoğu durumda, bir grubun kodunu diğer grubun projelerinde bile kullanabilirsiniz. Bu kodun kullanılması unit()durumunda, 0 durumunda bir bölmede ne olduğunu gerçekten umursuyorsanız, testi kendiniz yazmalısınız. Dolayısıyla, onu yalnızca kötü sonuçlar aldığınız gömülü bir sisteme taşırsanız, bu davranış "aklı başında" olur. Şimdi, iş perspektifinden kodlayıcıları bir kez eğitiyorum ve işimin tüm bölümlerinde aynı API'leri ve aynı stilleri kullanıyorlar.

Çoğu durumda, başkalarının tavsiyelerine uymak istersiniz: tryGetUnit()istisnalar atmayan işlevler için benzer veya benzeri deyimler kullanın ve bunun yerine boş gibi bir sentinel değeri döndürün. Kullanıcıların tek bir programda hem istisna işleme hem de istisna dışı işleme kodunu yan yana kullanmak isteyebileceğini düşünüyorsanız, tryGetUnit()gösterime devam edin. Ancak, işletme gerçekliğinin bayrak kullanmayı daha iyi sağlayabileceği bir köşe durumu var. Kendinizi o köşede bulursanız, "kuralları" yan yana bırakmaktan korkmayın. Bunun için kurallar!

Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.