impl<T> Drop for List<T> why need two T

在实现泛型功能的时候为什么需要传入两个 泛型标记,如果只是知道结构内存大小、结构名称以及函数名的具体实现一个T不就可以了? Why do we need to pass in two generic tags when implementing the generic function? If we only know the structure memory size, structure name and the specific implementation of function name, a T is OK?

If your question is why the Rust syntax

impl<T> Drop for List<T> {
    fn drop(&mut self) { … }
}

contains the letter T twice, namely

impl<T> Drop for List<T> {
     ^                ^
     |                |
     +----------------+-- here 

that’s explained easily. The first T is introducing the generic type parameter, and the second T is using it.

The situation is comparable to why

fn f(x: i32) -> i32 { x + 1 }

contains the letter x twice, namely

fn f(x: i32) -> i32 { x + 1 }
     ^                ^
     |                |
     +----------------+-- here 

In a plain English interpretation of the syntax, you could read these examples as something like

“Define the function f to, given any integer x, return the integer x + 1”.

and

For every type T, implement the trait Drop for the type List<T>.

Each of these plain English sentences also mention the letter x or T respectively twice. Once to introduce and explain what x or T is supposed to be, and once to make sensible use of the previously introduced variable.

Also note, that the choice of name of these variables is irrelevant. The fact that they’re single-letter names is irrelevant, and any name could have been chosen. Even names of other existing types would be permitted, albeit confusing, e.g.

struct Foo<T>(T);

impl<String> Drop for Foo<String> {
    fn drop(&mut self) {}
}

^^^ this has nothing to do with the standard library type std::string::String

Using the name only once would have a totally different meaning, and

impl Drop for Foo<String> {
    fn drop(&mut self) {}
}

would be writing the trait implementation only for the type Foo<std::string::String>, although in case of the special trait Drop, such an implementation would actually be rejected.


By the way, please note that questions like this are a better fit for users.rust-lang.org. The internals.rust-lang.org forum is mainly concerned with discussing/proposing things like language design, compiler implementation, or standard library API or implementation.

14 Likes

OK, this is my first time to enter the community and ask questions, ^~^! Next time I will ask such questions at users.trust-lang.org. Can you recommend a Rust book like C++<(Inside the C++Object Model>) to me? You just mean definition and use or declaration and use. I want to know more details. Why should we declare it this way, instead of making a generic T like C++, and how does Rust use T.

For learning Rust, the most commonly recommended learning resource to learn the basics is the book “The Rust Programming Language” https://doc.rust-lang.org/book; there’s also a community-translated Chinese version here.

The relevant chapter talking about generics is chapter 10 (Chinese version).

1 Like

我觉得 @steffahn 的回答已经足够好了。我建议你仔细再读一遍。

C++里面类的定义和成员函数必须同时声明, 所以没这个需求。但是Rust里结构体的定义和成员函数的定义(impl块) 是分开的, 所以你需要消歧义。

或者具体来说, 你需要区分这两个情况:

struct T;
impl Foo<T> {}  // #1
impl<T> Foo<T> {}  // #2

TR:

I think @steffahn's answer is good enough. I suggest you read it again carefully.

Class definitions and member functions in C++ must be declared at the same time, so there is no such requirement. But in Rust the definition of the struct is separate from the definition of member functions (impl blocks), so you need disambiguation.

Or specifically, you need to differentiate between these two cases:

...

1 Like

I’m not too deeply familiar with C++, however the most similar language feature, C++ template, also features syntax where type parameters are introduced in one place and then used in another place. For example

template <typename T>
void my_drop_function(Foo<T>& this) { … }

introduces T in the first line and uses it in the second line.

Free standing functions in Rust work similarly syntax-wise, e.g. the above could be something as follows in Rust

fn my_drop_function<T>(this: &mut Foo<T>) { … }

Now, impl blocks and associated items are a feature a bit more unique to Rust (especially in how the syntax looks and how they are separate from class declarations) but essentially, a trait implementation like

impl<T> Drop for Foo<T> {
    fn drop(self: &mut Foo<T>) { … }
    // `self: &mut Foo<T>` here is a more explicit equivalent
    // alternative to writing `self: &mut Self` or just `&mut self` 
}

is somewhat comparable to a definition of a free-standing drop function, in particular in that the T parameter used in the self argument needs to be introduced somewhere. Just now there’s two places where generic arguments can be introduced. One place is for the whole impl block, and the other place is on the individual function/method itself. The type arguments on the impl itself also play a role in trait resolution, and type arguments used in the Self-type need to be introduced on this outer level.

The reason why a struct definition syntax (and similarly a C++ templated class definition) can get away with not needing to type out type parameters twice is because it’s less flexible. If you define a struct in Rust

struct Foo<T> {
    field1: Option<T>,
    field2: (u32, Box<T>),
}

is mostly simply just because the struct definition is always fully generic over every possible type given as parameter, whereas impl blocks can be more restrictive. For a struct Foo<A, B>(…);, an impl block can be fully generic

impl<A, B> Foo<A, B> { … }
// or
impl<A, B> SomeTrait for Foo<A, B> { … }

in which case, I do admit the syntax looks slightly repetitive (though one gets used to it), but this explicitness allows more flexibility; we can restrict the impl if we want, e.g. to only a concrete type

impl Foo<String, bool> { … }

or only to instances where both types are the same

impl<A> Foo<A, A> { … }

or any other arbitrarily complex situation, e.g.

impl<X, Y, Z> Foo<Bar<X, Z>, Baz<Y, X>> { … }
4 Likes

Actually, C++ is a bit lax here. If you have something like

template<typename T>
class Foo {
    bool operator+=(Foo other);
}

you don't need to provide template arguments to the current class type, and if you don't, it's implicitly understood to use the same template arguments as the primary template. The closest analogue to implementing a trait for some type in C++ would likely be adding a template specialization, e.g.

template<typename T>
void std::swap(Foo<T>& x, Foo<T>& y);

which does require repeating the template parameters.

1 Like

理解了 :+1:

:+1:

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.