RPIT Ömür Yakalama Kuralları
Bu bölüm, RFC 3498 ile gelen 2024 Ömür Yakalama Kuralları ile ilgili değişiklikleri açıklar. Ayrıca kodunuzu taşırken opak türlerde kesin yakalama özelliğini, yani RFC 3617 ile gelen yaklaşımı nasıl kullanacağınızı da anlatır.
Özet
- Rust 2024’te,
use<..>sınırı yoksa ömür parametreleri dahil kapsam içindeki tüm jenerik parametreler örtük olarak yakalanır. Captureshilesi (Captures<..>sınırları) ve outlives hilesi (örneğin'_sınırları),use<..>sınırlarıyla değiştirilebilir ya da Rust 2024’te tamamen kaldırılabilir.
Ayrıntılar
Yakalama
Bir RPIT (return-position impl Trait) opak türünde bir jenerik parametreyi
yakalamak, ilgili gizli tür içinde o parametrenin kullanılabilmesini sağlar.
Rust 1.82’de hangi jenerik parametrelerin yakalanacağını açıkça belirtmeye
yarayan use<..> sınırları eklendi. Bunlar hem kodunuzu Rust 2024’e taşırken
işe yarar hem de bu bölümde sürüme özgü örtük yakalama kurallarını açıklamayı
kolaylaştırır. use<..> sınırları şu şekilde görünür:
#![allow(unused)]
fn main() {
fn capture<'a, T>(x: &'a (), y: T) -> impl Sized + use<'a, T> {
// ~~~~~~~~~~~~~~~~~~~~~~~
// Bu, RPIT opak türüdür.
//
// `'a` ile `T`yi yakalar.
(x, y)
//~~~~~~
// Gizli tür şudur: `(&'a (), T)`.
//
// Bu tür `'a` ile `T`yi kullanabilir; çünkü yakalanmışlardır.
}
}
Yakalanan jenerik parametreler, opak türün nasıl kullanılabileceğini etkiler. Örneğin aşağıdaki kullanım hatadır; çünkü gizli tür ömrü kullanmasa bile ömür yakalanmıştır:
#![allow(unused)]
fn main() {
fn capture<'a>(_: &'a ()) -> impl Sized + use<'a> {}
fn test<'a>(x: &'a ()) -> impl Sized + 'static {
capture(x)
//~^ HATA ömür yeterince uzun yaşamıyor olabilir
}
}
Buna karşılık şu kullanım geçerlidir:
#![allow(unused)]
fn main() {
fn capture<'a>(_: &'a ()) -> impl Sized + use<> {}
fn test<'a>(x: &'a ()) -> impl Sized + 'static {
capture(x) //~ TAMAM
}
}
use<..> sınırı yokken sürüme özgü kurallar
use<..> sınırı yoksa, derleyici kapsam içindeki hangi jenerik parametrelerin
örtük biçimde yakalanacağına karar vermek için sürüme özgü kuralları kullanır.
Tüm sürümlerde, use<..> sınırı yoksa kapsam içindeki tüm tür ve const jenerik
parametreleri örtük olarak yakalanır. Örneğin:
#![allow(unused)]
fn main() {
fn f_implicit<T, const C: usize>() -> impl Sized {}
// ~~~~~~~~~~
// Burada `use<..>` sınırı yok.
//
// Tüm sürümlerde yukarıdaki tanım şuna denktir:
fn f_explicit<T, const C: usize>() -> impl Sized + use<T, C> {}
}
Rust 2021 ve daha eski sürümlerde, use<..> sınırı yokken jenerik ömür
parametreleri yalnızca çıplak fonksiyon imzalarındaki ve inherent impl içindeki
ilişkili fonksiyon/metod imzalarındaki RPIT opak türlerde bir sınır içinde
sözdizimsel olarak göründüklerinde yakalanır. Rust 2024’ten itibaren ise kapsam
içindeki bu jenerik ömür parametreleri koşulsuz yakalanır. Örneğin:
#![allow(unused)]
fn main() {
fn f_implicit(_: &()) -> impl Sized {}
// Rust 2021 ve öncesinde yukarıdaki tanım şuna denktir:
fn f_2021(_: &()) -> impl Sized + use<> {}
// Rust 2024 ve sonrasında ise şuna denktir:
fn f_2024(_: &()) -> impl Sized + use<'_> {}
}
Bu değişiklik, trait impl içindeki ilişkili fonksiyon ve metod imzalarındaki
RPIT opak türlerle, trait tanımları içindeki RPIT kullanımlarıyla (RPITIT) ve
async fn tarafından oluşturulan opak Future türleriyle davranışı tutarlı
hale getirir. Bunların hepsi, use<..> sınırı olmadığında tüm sürümlerde
kapsam içindeki tüm jenerik ömür parametrelerini örtük olarak yakalar.
Dış jenerik parametreler
Dış bir impl’den gelen jenerik parametreler de örtük olarak neyin yakalanacağına karar verilirken kapsam içinde kabul edilir. Örneğin:
#![allow(unused)]
fn main() {
struct S<T, const C: usize>((T, [(); C]));
impl<T, const C: usize> S<T, C> {
// ~~~~~~~~~~~~~~~~~
// Bu jenerik parametreler kapsam içindedir.
fn f_implicit<U>() -> impl Sized {}
// ~ ~~~~~~~~~~
// ^ Bu jenerik de kapsam içindedir.
// ^
// |
// Burada `use<..>` sınırı yok.
//
// Tüm sürümlerde şuna denktir:
fn f_explicit<U>() -> impl Sized + use<T, U, C> {}
}
}
Higher-ranked bağlayıcılardan gelen ömürler
Aynı şekilde, higher-ranked bir for<..> bağlayıcısı tarafından kapsama sokulan
jenerik ömür parametreleri de kapsam içinde kabul edilir. Örneğin:
#![allow(unused)]
fn main() {
trait Tr<'a> { type Ty; }
impl Tr<'_> for () { type Ty = (); }
fn f_implicit() -> impl for<'a> Tr<'a, Ty = impl Copy> {}
// Rust 2021 ve öncesinde yukarıdaki tanım şuna denktir:
fn f_2021() -> impl for<'a> Tr<'a, Ty = impl Copy + use<>> {}
// Rust 2024 ve sonrasında ise şuna denktir:
//fn f_2024() -> impl for<'a> Tr<'a, Ty = impl Copy + use<'a>> {}
// ~~~~~~~~~~~~~~~~~~~~
// Ancak iç içe opak türlerde higher-ranked ömürlerin
// yakalanması henüz desteklenmiyor.
}
Argüman konumundaki impl Trait (APIT)
APIT (argument position impl Trait) kullanımıyla oluşturulan anonim, yani adsız jenerik parametreler de kapsam içinde kabul edilir. Örneğin:
#![allow(unused)]
fn main() {
fn f_implicit(_: impl Sized) -> impl Sized {}
// ~~~~~~~~~~
// Buna APIT denir.
//
// Yukarıdaki tanım kabaca şuna denktir:
fn f_explicit<_0: Sized>(_: _0) -> impl Sized + use<_0> {}
}
İlkinin ikincisine tam olarak denk olmadığını unutmayın; çünkü jenerik
parametreye ad verdiğinizde artık ona turbofish sözdizimiyle açıkça argüman
verilebilir. Adsız bir jenerik parametreyi use<..> sınırına açık biçimde
eklemenin, onu adlandırılmış jenerik parametreye çevirmek dışında bir yolu yoktur.
Taşıma
Fazla yakalamadan kaçınarak taşıma
impl_trait_overcaptures lint’i, Rust 2024’te ek ömürler yakalayacak RPIT opak
türleri işaretler. Bu lint, cargo fix --edition çalıştırıldığında otomatik
olarak uygulanan rust-2024-compatibility lint grubunun parçasıdır. Çoğu
durumda lint, Rust 2024’te fazladan ömür yakalanmaması için gereken yerlere
otomatik olarak use<..> sınırları ekleyebilir.
Kodunuzu Rust 2024 ile uyumlu hale getirmek için şunu çalıştırın:
cargo fix --edition
Örneğin bu komut şunu:
#![allow(unused)]
fn main() {
fn f<'a>(x: &'a ()) -> impl Sized { *x }
}
şuna dönüştürür:
#![allow(unused)]
fn main() {
fn f<'a>(x: &'a ()) -> impl Sized + use<> { *x }
}
Bu use<> sınırı eklenmezse, Rust 2024’te opak tür 'a ömür parametresini
yakalar. Bu sınır eklenerek taşıma lint’i mevcut anlamı korur.
APIT içeren durumları taşıma
Bazı durumlarda lint, değişikliği otomatik yapamaz; çünkü use<..> sınırı
içinde yer alabilmesi için bir jenerik parametreye ad verilmesi gerekir. Böyle
durumlarda lint, elle bir değişiklik yapmanız gerekebileceğini bildirir. Örneğin:
#![allow(unused)]
fn main() {
fn f<'a>(x: &'a (), y: impl Sized) -> impl Sized { (*x, y) }
// ^^ ~~~~~~~~~~
// Bu bir APIT kullanımıdır.
//
//~^ UYARI `impl Sized`, 2024 sürümünde istenenden daha fazla ömür yakalayacak olabilir
//~| NOT özellikle bu ömür kapsam içinde ama türün sınırlarında belirtilmiyor
fn test<'a>(x: &'a (), y: ()) -> impl Sized + 'static {
f(x, y)
}
}
Bu kod, APIT kullanımı nedeniyle ve jenerik tür parametresinin use<..>
sınırında adlandırılması gerektiği için otomatik olarak dönüştürülemez. Bu
kodu ömrü yakalamadan Rust 2024’e çevirmek için tür parametresine ad vermelisiniz.
Örneğin:
#![allow(unused)]
fn main() {
#![deny(impl_trait_overcaptures)]
fn f<'a, T: Sized>(x: &'a (), y: T) -> impl Sized + use<T> { (*x, y) }
// ~~~~~~~~
// Tür parametresi burada adlandırıldı.
fn test<'a>(x: &'a (), y: ()) -> impl Sized + use<> {
f(x, y)
}
}
Bunun fonksiyon API’sini az da olsa değiştirdiğini unutmayın; çünkü artık bu
parametre için turbofish sözdizimiyle açık tür argümanı verilebilir. Bu istenmiyorsa,
use<..> sınırını eklememeyi ve ömrün yakalanmasına izin vermeyi de
düşünebilirsiniz. Özellikle ileride bu ömrü gizli tür içinde kullanmak
isteyebileceğinizi düşünüyorsanız bu daha uygun olabilir.
Captures hilesinden vazgeçerek taşıma
Rust 1.82’de gelen kesin yakalama use<..> sınırlarından önce, bir RPIT opak
türde bir ömrü doğru biçimde yakalamak çoğu zaman Captures hilesini gerektirirdi.
Örneğin:
#![allow(unused)]
fn main() {
#[doc(hidden)]
pub trait Captures<T: ?Sized> {}
impl<T: ?Sized, U: ?Sized> Captures<T> for U {}
fn f<'a, T>(x: &'a (), y: T) -> impl Sized + Captures<(&'a (), T)> {
// ~~~~~~~~~~~~~~~~~~~~~
// Buna `Captures` hilesi denir.
(x, y)
}
fn test<'t, 'x>(t: &'t (), x: &'x ()) {
f(t, x);
}
}
use<..> sınırı sözdizimiyle birlikte Captures hilesine artık gerek kalmadı.
Tüm sürümlerde şu biçimle değiştirilebilir:
#![allow(unused)]
fn main() {
fn f<'a, T>(x: &'a (), y: T) -> impl Sized + use<'a, T> {
(x, y)
}
fn test<'t, 'x>(t: &'t (), x: &'x ()) {
f(t, x);
}
}
Rust 2024’te use<..> sınırı çoğu zaman tamamen atlanabilir ve yukarıdaki örnek
basitçe şöyle yazılabilir:
#![allow(unused)]
fn main() {
fn f<'a, T>(x: &'a (), y: T) -> impl Sized {
(x, y)
}
fn test<'t, 'x>(t: &'t (), x: &'x ()) {
f(t, x);
}
}
Bunun için otomatik taşıma yoktur ve Captures hilesi Rust 2024’te hâlâ
çalışır; ancak bu eski hileden elle uzaklaşmayı düşünmek isteyebilirsiniz.
Outlives hilesinden vazgeçerek taşıma
Rust 1.82’de kesin yakalama use<..> sınırları gelmeden önce, bir ömrün bir
opak türün gizli türünde kullanılması gerektiğinde “outlives hilesi” sıkça
kullanılırdı. Örneğin:
#![allow(unused)]
fn main() {
fn f<'a, T: 'a>(x: &'a (), y: T) -> impl Sized + 'a {
// ~~~~ ~~~~
// ^ Bu, outlives hilesidir.
// |
// Bu sınır yalnızca bu hile için gereklidir.
(x, y)
// ~~~~~~
// Gizli tür `(&'a (), T)`dir.
}
}
Bu hile, Captures hilesine göre daha az dolambaçlıydı; ama aynı zamanda daha
az doğruydu. Yukarıdaki örnekte gördüğümüz gibi, T içindeki ömür bileşenleri
'a ömründen bağımsız olsa bile bu hileyi çalıştırmak için T: 'a sınırı
eklemek zorunda kalıyoruz. Bu da çağıranlar üzerinde gereksiz ve şaşırtıcı
kısıtlamalar yaratıyordu.
Kesin yakalama kullanıldığında, yukarıdaki örnek tüm sürümlerde şu şekilde yazılabilir:
#![allow(unused)]
fn main() {
fn f<T>(x: &(), y: T) -> impl Sized + use<'_, T> {
(x, y)
}
fn test<'t, 'x>(t: &'t (), x: &'x ()) {
f(t, x);
}
}
Rust 2024’te use<..> sınırı çoğu zaman tamamen atlanabilir ve yukarıdaki örnek
basitçe şöyle yazılabilir:
#![allow(unused)]
fn main() {
fn f<T>(x: &(), y: T) -> impl Sized {
(x, y)
}
fn test<'t, 'x>(t: &'t (), x: &'x ()) {
f(t, x);
}
}
Bunun için otomatik taşıma yoktur ve outlives hilesi Rust 2024’te hâlâ çalışır; ama bu eski hileden elle uzaklaşmayı düşünmek isteyebilirsiniz.