22 Mart 2024
Bu makalede, çevirmenimizin sınıf üyelerini, değişkenleri, alanları, operatörleri ve C# kontrol yapılarını nasıl dönüştürdüğünü inceleyeceğiz. Ayrıca, .NET Framework türlerini doğru bir şekilde C++'a dönüştürmek için çevirmen destek kütüphanesinin kullanımına da değineceğiz.
Sınıf yöntemleri doğrudan C++'a eşlenir. Bu, aynı zamanda statik yöntemler ve yapıcılar için de geçerlidir. Bazı durumlarda, örneğin statik yapıcıları çağırmak için ek kod görünebilir. Uzantı yöntemleri ve operatörler, statik yöntemlere dönüştürülür ve açıkça çağrılır. Finalize yöntemleri yıkıcılar haline gelir.
C# örnek alanları C++ örnek alanlarına dönüşür. Statik alanlar da değişmez, ancak başlatma sırasının önemli olduğu durumlarda (bu, böyle alanların tek örnek olarak çevrildiği şekilde uygulanır).
Özellikler, bir alıcı yöntemi ve bir ayarlayıcı yöntemi olarak bölünür veya ikinci yöntem yoksa sadece bir yöntem olarak kalır. Otomatik özellikler için özel bir değer alanı da eklenir. Statik özellikler, bir statik alıcı ve ayarlayıcı olarak bölünür. İndeksleyiciler aynı mantıkla işlenir.
Etkinlikler, türü System::Event
'in gerekli özelleştirmesine karşılık gelen alanlara çevrilir. Üç yöntemli bir çeviri (add
, remove
ve invoke
) daha doğru olurdu ve ayrıca soyut ve sanal etkinlikleri desteklememizi sağlardı. Muhtemelen gelecekte böyle bir modele ulaşacağız, ancak şu anda Event
sınıf seçeneği ihtiyaçlarımızı tamamen karşılamaktadır.
Aşağıdaki örnek, yukarıdaki kuralları göstermektedir:
public abstract class Generic<T>
{
private T m_value;
public Generic(T value)
{
m_value = value;
}
~Generic()
{
m_value = default(T);
}
public string Property { get; set; }
public abstract int Property2 { get; }
public T this[int index]
{
get
{
return index == 0 ? m_value : default(T);
}
set
{
if (index == 0)
m_value = value;
else
throw new ArgumentException();
}
}
public event Action<int, int> IntIntEvent;
}
C++ çeviri sonucu (önemsiz kodlar çıkarıldı):
template<typename T>
class Generic : public System::Object
{
public:
System::String get_Property()
{
return pr_Property;
}
void set_Property(System::String value)
{
pr_Property = value;
}
virtual int32_t get_Property2() = 0;
Generic(T value) : m_value(T())
{
m_value = value;
}
T idx_get(int32_t index)
{
return index == 0 ? m_value : System::Default<T>();
}
void idx_set(int32_t index, T value)
{
if (index == 0)
{
m_value = value;
}
else
{
throw System::ArgumentException();
}
}
System::Event<void(int32_t, int32_t)> IntIntEvent;
virtual ~Generic()
{
m_value = System::Default<T>();
}
private:
T m_value;
System::String pr_Property;
};
Sabit ve statik alanlar, statik alanlar, statik sabitler (bazı durumlarda constexpr
) veya bir singleton'a erişim sağlayan statik yöntemlere çevrilir. C# örnek alanları C++ örnek alanlarına dönüştürülür. Herhangi bir karmaşık başlatıcı, yapıcıya taşınır ve bazen C# içinde mevcut olmayan varsayılan yapıcıları açıkça eklemek gerekebilir. Yığın değişkenleri olduğu gibi iletilir. Yöntem argümanları da olduğu gibi iletilir, ancak hem ref
hem de out
argümanları referanslara dönüşür (neyse ki bunlar üzerinde aşırı yükleme yasaktır).
Alanların ve değişkenlerin türleri C++ eşdeğerleriyle değiştirilir. Çoğu durumda, bu tür eşdeğerler, çevirmen tarafından C# kaynak kodundan kendiliğinden oluşturulur. .NET Framework türleri ve bazı diğerleri dahil olmak üzere kütüphane türleri, C++'da bizim tarafımızdan yazılır ve çevrilmiş ürünlerle birlikte sağlanan çevirmen destek kütüphanesinin bir parçasıdır. var
, davranış farklılıklarını düzeltmek için açık tür belirtimi gerektiği durumlar dışında auto
'ya çevrilir.
Ayrıca, referans türleri SmartPtr
ile sarmalanır. Değer türleri olduğu gibi yerine konur. Tür argümanları değer veya referans türleri olabilir, bu nedenle bunlar da olduğu gibi yerine konur, ancak örneklendirildiğinde referans argümanları SharedPtr
ile sarmalanır. Böylece, List<int>
List<int32_t>
olarak çevrilir, ancak List<Object>
List<SmartPtr<Object>>
haline gelir. Bazı istisnai durumlarda, referans türleri değer türleri olarak çevrilir. Örneğin, System::String
uygulamamız, ICU kaynaklı UnicodeString
türüne dayanır ve yığın depolama için optimize edilmiştir.
Örneklemek için aşağıdaki sınıfı çevirelim:
public class Variables
{
public int m_int;
private string m_string = new StringBuilder().Append("foobazz").ToString();
private Regex m_regex = new Regex("foo|bar");
public object Foo(int a, out int b)
{
b = a + m_int;
return m_regex.Match(m_string);
}
}
Çeviri sonrası, aşağıdaki şekli alır (önemsiz kodlar çıkarıldı):
class Variables : public System::Object
{
public:
int32_t m_int;
System::SharedPtr<System::Object> Foo(int32_t a, int32_t& b);
Variables();
private:
System::String m_string;
System::SharedPtr<System::Text::RegularExpressions::Regex> m_regex;
};
System::SharedPtr<System::Object> Variables::Foo(int32_t a, int32_t& b)
{
b = a + m_int;
return m_regex->Match(m_string);
}
Variables::Variables()
: m_int(0)
, m_regex(System::MakeObject<System::Text::RegularExpressions::Regex>(u"foo|bar"))
{
this->m_string = System::MakeObject<System::Text::StringBuilder>()->
Append(u"foobazz")->ToString();
}
Ana kontrol yapılarının benzerliği işimize yarıyor. if
, else
, switch
, while
, do
-while
, for
, try
-catch
, return
, break
ve continue
gibi operatörler çoğunlukla olduğu gibi aktarılır. Bu listedeki tek istisna muhtemelen switch
operatörüdür ve özel birkaç işlem gerektirir. İlk olarak, C# onun dize türüyle kullanılmasına izin verir—C++'da bu durumda bir if
-else if
dizisi oluştururuz. İkincisi, kontrol edilen ifadenin bir tür şablonuna eşleştirilme yeteneğinin nispeten yeni bir eklemesi vardır—ancak bu da kolayca bir dizi if
'e açılabilir.
C++'da bulunmayan yapılar da ilgi çekicidir. Örneğin, using
operatörü, bağlamdan çıkarken Dispose()
yönteminin çağrısını garanti eder. C++'da bu davranışı, yığın üzerinde bir koruma nesnesi oluşturarak taklit ediyoruz ve bu nesnenin yıkıcısında gerekli yöntemi çağırıyoruz. Ancak bundan önce, using
'in gövdesi olan kod tarafından atılan istisnayı yakalamak ve koruma nesnesinin alanına exception_ptr
'yi saklamak gereklidir—eğer Dispose()
kendi istisnasını atmazsa, sakladığımız istisna yeniden atılacaktır. Bu, yıkıcıdan istisna atmanın haklı çıkarıldığı ve hata olmadığı nadir bir durumdur. finally
bloğu benzer bir düzene göre çevrilir, ancak Dispose()
yöntemi yerine, çevirmenin gövdesini sardığı bir lambda işlevi çağrılır.
C# dilinde bulunmayan ve zorunda kaldığımız bir diğer operatör foreach
'tir. Başlangıçta, bunu eşdeğer bir while
döngüsüne çevirdik ve sayacın MoveNext()
yöntemini çağırdık. Bu yöntem evrensel olmakla birlikte oldukça yavaştır. .NET konteynerlerinin çoğu C++ uygulaması STL veri yapılarını kullandığından, mümkün olduğunda orijinal iteratörlerini kullanmaya başladık ve foreach
'ü aralık tabanlı for
döngüsüne dönüştürdük. Orijinal iteratörlerin kullanılamadığı durumlarda (örneğin, konteyner saf C# ile uygulanmışsa), içsel olarak numaralandırıcılarla çalışan sarmal iteratörler kullanılır. Daha önce doğru iterasyon yönteminin seçimi, SFINAE tekniği kullanılarak yazılmış harici bir işlevin sorumluluğuydu, şimdi ise tüm konteynerlerde, çevrilenler dahil, begin
-end
yöntemlerinin doğru sürümlerine sahip olmaya yakınız.
Kontrol yapılarıyla aynı şekilde, çoğu operatör (en azından aritmetik, mantıksal ve atama operatörleri) özel işlem gerektirmez. Ancak, ince bir nokta vardır: C# dilinde bir ifadenin parçalarının değerlendirme sırası belirlidir, oysa C++ dilinde bazı durumlarda tanımsız davranış olabilir. Örneğin, aşağıdaki çevrilmiş kod, farklı araçlar tarafından derlendikten sonra farklı davranır:
auto offset32 = block[i++] + block[i++] * 256 + block[i++] * 256 * 256 +
block[i++] * 256 * 256 * 256;
Neyse ki, böyle sorunlar oldukça nadirdir. Bu tür anları ele almak için çevirmeni eğitmek için planlarımız var, ancak yan etkileri olan ifadeleri tanımlayan analizin karmaşıklığı nedeniyle henüz uygulanmadı.
Ancak, en basit operatörler bile özellikle özelliklere uygulandığında özel işlem gerektirir. Yukarıda gösterildiği gibi, özellikler alıcılar ve ayarlayıcılar olarak ayrılır ve çevirmen, bağlama bağlı olarak gerekli çağrıları eklemek zorundadır:
obj1.Property = obj2.Property;
string s = GetObj().Property += "suffix";
obj1->set_Property(obj2->get_Property());
System::String s = System::setter_add_wrap(static_cast<MyClass*>(GetObj().GetPointer()),
&MyClass::get_Property, &MyClass::set_Property, u"suffix")
İlk satırda, yerine koyma işlemi basit çıktı. İkincisinde, setter_add_wrap
sarmalını kullanmak gerekiyordu, böylece GetObj()
işlevinin yalnızca bir kez çağrıldığından emin olunur ve get_Property()
çağrısının ve dize harfle birleştirilmiş sonucunun yalnızca set_Property()
yöntemine (ki bu void
döndürür) değil, aynı zamanda ifadede daha fazla kullanım için geçirilir. Aynı yaklaşım, indeksleyicilere erişirken de uygulanır.
C++ dilinde olmayan C# operatörleri (as
, is
, typeof
, default
, ??
, ?.
, vb.) çevirmen destek kütüphanesi işlevlerini kullanarak taklit edilir. Argümanların çift değerlendirmesinden kaçınılması gereken durumlarda, örneğin GetObj()?.Invoke()
'u GetObj() ? GetObj().Invoke() : nullptr
şeklinde açmamak için yukarıda gösterilen yaklaşım benzer şekilde kullanılır.
Üye erişim operatörü (.
), bağlama bağlı olarak C++'dan bir eşdeğeri ile değiştirilebilir: kapsam çözümleme operatörü (::
) veya “ok” (->
). Bu tür bir değiştirme, yapı üyelerine erişirken gereksiz değildir.