Rust:智能指针 Box & Rc & Cow

Rust:智能指针 Box & Rc & Cow


RAII可以说是现代非GC语言的核心内存管理方式,而智能指针可以说是RAII的嫡长子。Rust提供了非常多样的智能指针,在智能指针之外,也有大量类型使用了RAII机制,本博客讲解Box<T>Rc<T>两种智能指针。

注:本文所有源码来自1.90.0版本


Box

BoxRust最常见的智能指针,它设计简单,也因此比其它指针更加高效,当然功能性也会比较单一。

Box将一份数据开在堆区,并持有它的唯一所有权,当Box销毁则堆区的数据一起销毁。

常用接口

Box用法非常简单,通过Box<T>::new就可以将一个类型放在堆上,此外还有一些可能遇到的用法。

  • leak:释放所有权但不释放内存
rust 复制代码
pub fn leak<'a>(b: Self) -> &'a mut T
where
    A: 'a,

leak是一个比较特别的接口,因为它刻意造成了内存泄露。

查看函数签名,它接受一个Self,直接返回了&mut T可变借用。返回借用的生命周期为'a,与返回值和堆分配器有关,借用的生命周期不能长于堆分配器。

它往往用于构造'static生命周期的数据。想象一下,在堆区有这样一块内存,它不会被释放,只要程序存活它也就存活,这不就是static生命周期吗?这就是leak的作用,将一个堆区内存的借用返回,这个借用会一直有效,因为堆区的数据已经没有人去回收了。

  • into_raw:转换为裸指针,放弃所有权
rust 复制代码
fn into_raw(b: Self) -> *mut T

这是一个不太安全的操作,它让Box主动放弃所有权,并且返回一个原生指针。这一般用于某一些只能接受原生指针的场景。

  • from_raw:把裸指针重新包装为安全 Box
rust 复制代码
unsafe fn from_raw(raw: *mut T) -> Self

这个方法和刚才相反,将一个原生指针包装为Box。但是这是一个unsafe方法,因为最后Box会释放T的内存,如果它指向的内存不是来自于堆分配器,就会发生错误,这是不安全的。必须保证传入的指针原本就是从Box拆出来的。

  • into_inner:拿到内部的值
rust 复制代码
fn into_inner(boxed: Self) -> T

这个方法直接返回Box内部的T,返回值是一个值上下文。因此如果T实现了Copy就拷贝,如果没有实现则移动。那么这个方法就可能导致Box内部的所有权转移而整体失效,后续不能再通过Box访问数据。


结构

接下来看看它是如何实现的,定义如下:

rust 复制代码
pub struct Box<
    T: ?Sized,
    #[unstable(feature = "allocator_api", issue = "32838")] A: Allocator = Global,
>(Unique<T>, A);

pub struct Unique<T: PointeeSized> {
    pointer: NonNull<T>,
    _marker: PhantomData<T>,
}

pub struct NonNull<T: PointeeSized> {
    pointer: *const T,
}

它的结构分为三层,首先Box本身是一个元组结构体,T是存储的数据,A是指定的内存分配器,默认使用全局Global

第二层是Uniuqe<T>,它内部是NonNull<T>,再内层则是一个*const T。也就是说最后的数据是用*const指针存储的,这也决定了Box<T>对于T是协变的。

既然已经保证协变了,为什么还需要一个PhantomData?此处的幻影类型,不是用于影响协变的,而是用于影响析构函数的。在复合类型析构函数中,会先调用所有字段的析构函数,最终调用自己的析构函数。此处_marker标识Unique<T>在逻辑上持有T,保证会先调用T的析构函数。


Drop

Box生命周期结束时,自动调用drop方法释放资源,在源码中它实现如下:

rust 复制代码
#[stable(feature = "rust1", since = "1.0.0")]
unsafe impl<#[may_dangle] T: ?Sized, A: Allocator> Drop for Box<T, A> {
    #[inline]
    fn drop(&mut self) {
        // the T in the Box is dropped by the compiler before the destructor is run

        let ptr = self.0;

        unsafe {
            let layout = Layout::for_value_raw(ptr.as_ptr());
            if layout.size() != 0 {
                self.1.deallocate(From::from(ptr.cast()), layout);
            }
        }
    }
}

其实还是非常简单的,拿到self.0的指针,然后通过self.1拿到堆分配器,调用dealloc把这个堆区释放掉。


Deref & DerefMut

这个Trait是第一次见,Deref意为解引用,它允许智能指针可以模仿指针的行为,从而语法上接近指针。

实现如下:

rust 复制代码
impl<T: ?Sized, A: Allocator> Deref for Box<T, A> {
    type Target = T;

    fn deref(&self) -> &T {
        &**self
    }
}

impl<T: ?Sized, A: Allocator> DerefMut for Box<T, A> {
    fn deref_mut(&mut self) -> &mut T {
        &mut **self
    }
}

当调用*box解引用时,相当于调用*(box.deref()),因此返回的借用可以被正常处理。

如实现了Deref,那么let r = *box就相当于调用box.deref()方法获取不可变借用。而如果实现了DerefMut*box = other就相当于调用box.deref_mut获取可变借用,看起来就和指针一样。

Deref返回的表达式&**self其实有一些蹊跷,在*box时,deref方法内部拿到的是&self,第一次*self相当于拿到了box本身,也就是*self等效于box

奇怪的地方来了,**self那不就是*box吗?*box难道不会递归调用自己的deref方法吗?这是一个死递归。

实际上,在Box内部解引用自己,并不是调用的deref方法。而是相当于直接拿到内部的T,这是对智能指针的特殊处理。因此**self相当于T,最后&**self就是&T得到不可变借用。可变借用同理。


Rc

Box是独占所有权的指针,保证堆区数据只被一个变量所有。如果需要多个智能指针指向共享一个数据,就需要Rc

Rc通过维护计数器来统计当前有多少个指针指向同一份数据,每当一个Rc离开生命周期,drop时减少一个计数。当计数为0,说明不存在任何Rc指向该数据,就可以销毁堆区的数据了。

常用接口

  • new :创建一个Rc,计数器初始化为1
rust 复制代码
fn new(value: T) -> Rc<T>
  • clone :拷贝一个Rc,计数器自增
rust 复制代码
fn clone(&self) -> Self

调用clone方法会产生一个新的Rc,但是内部的数据不会发生拷贝,多个Rc指向同一个数据。

  • strong_count :获取计数器当前数值,即当前有几个Rc指向同一数据
rust 复制代码
fn strong_count(this: &Self) -> usize

注意这个函数第一个参数不是self,因此必须通过Rc::strong_count进行调用。

写个例子使用以上三个接口:

rust 复制代码
fn main() {
    let a = Rc::new(String::from("hello"));

    {
        let b = a.clone(); // count += 1
        println!("{}", Rc::strong_count(&a));
    } // count -= 1

    println!("{}", Rc::strong_count(&a));
}

首先通过Rc::new创建一个字符串,内部作用域b = a.clone()Rc进行了拷贝,但是指向的还是同一份数据。最后在作用域内外分别输出计数器的数值,输出值为21


Weak

为什么借用计数要交strong_count,难道还有一个weak_count吗?没错,Rc有可能导致循环借用,这会导致内存泄漏,因此Rc内部维护了两套计数,分别用于强借用和弱借用。

看看以下代码,会产生什么问题:

rust 复制代码
struct Node {
    name: String,
    next: RefCell<Option<Rc<Node>>>,
}

fn foo() {
    let a = Rc::new(Node {
        name: String::from("A"),
        next: RefCell::new(None),
    });

    let b = Rc::new(Node {
        name: String::from("B"),
        next: RefCell::new(None),
    });

    // A -> B
    *a.next.borrow_mut() = Some(Rc::clone(&b));

    // B -> A (形成循环!)
    *b.next.borrow_mut() = Some(Rc::clone(&a));

    println!("a strong count = {}", Rc::strong_count(&a));
    println!("b strong count = {}", Rc::strong_count(&b));
}

这是使用Rc实现的链表节点,next表示下一个节点。为了内部可变性,使用了RefCellOption则是保证其可以进行初始化为None,否则无法开出第一个节点。

foo函数中,首先创建了两个nextNone的节点,随后修改Rc的指向,使得A->BB->A,此时就形成了循环借用。

图中省略了多余的包装,只显示最底层的核心数据。

对于Node_A来说,它被变量ab.next同时指向,计数器为2。对于Node_B来说,它被变量ba.next同时指向,计数器为2

何时这两个数据才会被销毁?假设离开函数foo的作用域,此时ab销毁,内存布局变成下图:

此时不再有任何栈区的变量指向两份数据,但是Node_A依然被b.next指向,反之Node_B依然被a.next指向。

想要Node_A被销毁,那么计数器需要减为0,也就是b.next应该被销毁。而想要b.next销毁,就需要Node_B被销毁,那么需要计数器减为0,也就是a.next应该被销毁。而想要a.next销毁,则需要Node_A被销毁...

这是一个死循环,两者相互制约,卡着对方最后那一位计数,导致两者都无法被销毁,甚至你已经无法在栈区上找到变量去访问它们了,这就是循环借用导致的内存泄露。

但是有时候我们确实需要这种,结构体内部相互指向的结构,比如图。为了保证内存安全,RustRc<T>配套提供了weak<T>,它可以和Rc指向相同的数据,但是不占用strong_count计数器。

RcWeak内部,都会维护一个叫做RcInner的控制块,这个控制块内部包含strong_countweak_countT三个字段。也就是这个控制块维护了两个计数器和真正的数据T

规则如下:

  • 构造第一个Rc时,初始化strong_count = 1weak_count = 1
  • 当生成新的Rcstrong_count += 1
  • Rc离开作用域,strong_count -= 1
  • 当生成新的Weakweak_count += 1
  • Weak离开作用域,weak_count -= 1
  • 每当Rc离开作用域,检查:
    • 如果strong_count == 0,销毁T,但是不销毁控制块,weak_count -= 1
    • 如果weak_count == 0,销毁整个控制块
  • 每当Weak离开作用域,检查:
    • 如果strong_count != 0,什么也不做
    • 如果strong_count == 0 && weak_count == 0,销毁整个控制块(此时T已经被销毁)

这才是完整的Rc计数机制,控制块的其中一部分是T,它由Rc单独管理,只要没有Rc了,T就会被销毁。控制块的另一部分是strong_countweak_count,它由RcWeak共同管理,只有两个计数器都为0,才销毁控制块。

这有一个注意点,初始化Rcweak_count初始化为1,这个设计是表示Rc也参与weak_count计数,但是所有Rc只占用一次计数,保证控制块不会被释放。当strong_count == 0的时候,表示最后一个Rc也已经离开了,此时weak_count额外减少一次,相当于控制块此时完全交由Weak管理。

相关接口:

  • Rc::downgrade :通过Rc获得Weak
rust 复制代码
fn downgrade(this: &Self) -> Weak<T, A>

通过downgrade,可以直接拿到一个Rc对应的Weak,此时weak_count会增加。

  • Weak::upgrade :将Weak提升为Rc
rust 复制代码
fn upgrade(&self) -> Option<Rc<T, A>>

弱指针Weak无法直接访问数据T,它必须升级为Rc才能访问。此时就需要用到upgrade方法,它尝试将一个Weak升级为Rc,过程中会检查strong_count,如果strong_count = 0,说明T已经被销毁了,则返回None,反之升级成功,可以通过Rc来访问数据了。


结构

接下来看看RcWeak内部到底是如何实现的。

RcInner

RcWeak内部都维护了一个控制块RcInner,结构如下:

rust 复制代码
#[repr(C)]
struct RcInner<T: ?Sized> {
    strong: Cell<usize>,
    weak: Cell<usize>,
    value: T,
}

RcInner中,有两个计数器StrongWeak,以及存储的数值value,计数器使用Cell实现内部可变性。

这个RcInner实现了一个Trait,叫做RcInnerPtr,如下:

rust 复制代码
trait RcInnerPtr {
    fn weak_ref(&self) -> &Cell<usize>;
    fn strong_ref(&self) -> &Cell<usize>;
    
    fn strong(&self) -> usize;
    fn inc_strong(&self);
    fn dec_strong(&self);
    fn weak(&self) -> usize;
    fn inc_weak(&self);
    fn dec_weak(&self);
}

这个Trait中定义了八个方法,前两个需要实现者自己实现,后面六个有默认实现(我没有写出来)。

其实从方法名就可以知道这些方法的功能,比如inc_strong是增加strong计数,dec_weak是减少weak计数。

对于RcInner来说,它只要实现weak_refstrong_ref,也就是提供自己的两个计数的借用,剩下六个方法就可以生效。

但是为什么不直接impl RcInner,非要搞一个Trait间接实现这些接口?这是因为Rc还有一个兄弟Arc,它的计数规则和Rc一样,但是内部是多线程安全版本,它也要这一套强弱计数的接口,所以把它封装为了Trait。不论是Rc还是Arc,想要使用这个计数接口,只需要提供两个方法来返回计数器的借用,实现了代码复用。


Weak

Weak源代码如下:

rust 复制代码
#[stable(feature = "rc_weak", since = "1.4.0")]
#[rustc_diagnostic_item = "RcWeak"]
pub struct Weak<
    T: ?Sized,
    #[unstable(feature = "allocator_api", issue = "32838")] A: Allocator = Global,
> {
    ptr: NonNull<RcInner<T>>,
    alloc: A,
}

可以看到,Weak内部只有两个字段。ptr是指向RcInner控制块的指针,NonNull内部是*const RcInner<T>。而alloc是堆分配器。

还记得

接下来看看关键方法对ptr的操作:

  • downgrade
rust 复制代码
pub fn downgrade(this: &Self) -> Weak<T, A>
where
    A: Clone,
{
    this.inner().inc_weak();
    
    debug_assert!(!is_dangling(this.ptr.as_ptr()));
    Weak { ptr: this.ptr, alloc: this.alloc.clone() }
}

downgrade是最常用的创建Weak的方式,可以看到它内部调用了inc_weak(),这是将weak_count += 1的操作,也就是创建一个新的Weak时,弱计数器自增。

  • upgrade
rust 复制代码
pub fn upgrade(&self) -> Option<Rc<T, A>>
where
    A: Clone,
{
    let inner = self.inner()?;

    if inner.strong() == 0 {
        None
    } else {
        unsafe {
            inner.inc_strong();
            Some(Rc::from_inner_in(self.ptr, self.alloc.clone()))
        }
    }
}

ungraded通过一个Weak得到Rc。代码逻辑中,先检查strong计数,如果为0说明数据已经被释放了,返回None。反之则inc_strong增加一个强计数,并返回一个Rc

  • drop
rust 复制代码
fn drop(&mut self) {
    let inner = if let Some(inner) = self.inner() { inner } else { return };

    inner.dec_weak();
    if inner.weak() == 0 {
        unsafe {
            self.alloc.deallocate(self.ptr.cast(), Layout::for_value_raw(self.ptr.as_ptr()));
        }
    }
}

dropRAII的核心机制,当Weak离开作用域,先调用dec_weak()减少弱计数。如果减少后为0,说明没有任何一个Rc或者Weak指向控制块,此时调用dealloc回收控制块。

可以看到,在Weak内部,确实调用了RcInner进行计数统计,不论是各种RcWeak之间的转换,或者等到生命周期结束,都会有计数的变化。

另外的,其实存在一个Weak::new方法,它可以创建一个空的弱指针,不指向任何数据。但是Weak里面不是一个NonNull吗?怎么可能会有空指针?

Rust在此特地注释了,NonNull<RcInner>是存在空状态的,只要内部的*const取值为usize::MAX。因为RcInner的对齐数至少为2usize::MAX是一个奇数,指针不可能对齐到这个位置,因此是一个无效取值,就用usize::MAX来表示空指针了。


Rc

Rc源代码如下:

rust 复制代码
#[doc(search_unbox)]
#[rustc_diagnostic_item = "Rc"]
#[stable(feature = "rust1", since = "1.0.0")]
#[rustc_insignificant_dtor]
pub struct Rc<
    T: ?Sized,
    #[unstable(feature = "allocator_api", issue = "32838")] A: Allocator = Global,
> {
    ptr: NonNull<RcInner<T>>,
    phantom: PhantomData<RcInner<T>>,
    alloc: A,
}

Rc也维护了ptralloc,由于NonNull内部是原生指针,所以最后所有的RcWeak指向的都是同一个RcInner计数器。此外还有一个PhantomData幻影类型,表示Rc在逻辑上持有RcInner<T>

接下来看看Rc内部的核心方法:

  • new
rust 复制代码
pub fn new(value: T) -> Rc<T> {
    unsafe {
        Self::from_inner(
            Box::leak(Box::new(RcInner { strong: Cell::new(1), weak: Cell::new(1), value }))
                .into(),
        )
    }
}

new直接创建一个Rc,代码逻辑很简单,就是将strongweak计数器都初始化为1

如果你仔细观察,会发现它使用了Box::leak,这是之前说的让Box释放所有权但是不释放数据。也就是说,Rc没有自己封装堆区的开辟逻辑,而是借用Box完成内存开辟。

  • clone
rust 复制代码
fn clone(&self) -> Self {
    unsafe {
        self.inner().inc_strong();
        Self::from_inner_in(self.ptr, self.alloc.clone())
    }
}

clone也很简单,调用inc_strong()增加强计数。

  • drop
rust 复制代码
fn drop(&mut self) {
    unsafe {
        self.inner().dec_strong();
        if self.inner().strong() == 0 {
            self.drop_slow();
        }
    }
}

unsafe fn drop_slow(&mut self) {
    let _weak = Weak { ptr: self.ptr, alloc: &self.alloc };
    
    unsafe {
        ptr::drop_in_place(&mut (*self.ptr.as_ptr()).value);
    }
}

drop内部调用了一个drop_slow方法,我也粘出来了,这是我个人觉得整个Rc - Weak体系设计的最精妙的部分。

Rc离开作用域,会调用drop释放资源。它首先调用dec_strong()减少强计数,如果强计数为0,说明已经没有任何Rc指向数据了,此时可以销毁数据value,于是调用drop_slow进行销毁。

drop_slow中,先直接创建了一个Weak智能指针,而且是直接复用ptr,这次创建Weak没有增加weak计数器。随后调用drop_in_place,它销毁的是ptr.value,也就是只销毁了数据,但是没有销毁RcInner

看似简单的代码,却暗含很多蹊跷!为什么创建了一个_weak变量,什么也没做?为什么创建Weak不增加弱计数?

还记得吗,当strong_count = 0的时候,需要额外将weak_count -= 1,这是为了抵消最开始Rcnew的时候初始化的weak_count = 1。为什么drop_slow没有执行dec_weak()方法?

厉害的人已经反应过来了,这里是RAII

看似我创建了一个_weak弱指针,什么也没做。但是当_weak离开drop_slow函数会发生什么?会触发Weak::drop吧?

Weak_drop的逻辑是,先将weak_count -= 1,然后检查weak_countstrong_count,如果都为0就销毁整个RcInner。也就是说,在Weak::drop里面完成了两件事,一个是减少weak_count,这刚好可以抵消Rc::new时初始化的那个weak_count = 1。另一个是检查是否要释放数据。

回看Rc::drop,它只执行了两个操作,dec_strong()和销毁数据value。万一这是包括RcWeak在内,最后一个指向数据的指针,那么Rc::drop应该还需要负责RcInner的回收吧?

但是Rc::drop没有自己去干这些活,它通过创建一个不增加计数的Weak指针,并让它基于RAII触发Weak::drop同时完成了两件事:抵消初始的weak_count = 1,以及如果是最后一个指针则回收内存。

这个看似简单古怪的代码实际上非常精彩。一定要建立一种认知:RAII是一种思想,而非单纯的内存管理方案、

最后画一张图理解整个Rc-Weak体系:


Deref

最后看一个轻松的,RcDeref

rust 复制代码
impl<T: ?Sized, A: Allocator> Deref for Rc<T, A> {
    type Target = T;

    #[inline(always)]
    fn deref(&self) -> &T {
        &self.inner().value
    }
}

Rc只提供了Deref,解引用模仿指针行为。但是不提供DerefMut,因为Rc不是独占所有权的指针,获取可变借用可能会失败,不能直接返回&mut T,至少也要返回一个Option,但是DerefMut可不能返回option,因此没有实现。

如果需要获得可变借用,可以尝试get_mut方法:

rust 复制代码
fn get_mut(this: &mut Self) -> Option<&mut T>

它返回的就是Option<&mut T>,如果只有一个strong_count独占就返回Some,反之返回None表示现在不能获取可变借用。


Cow

Cow也是一个智能指针,它定义在std::borrow::Cow中,没有前两者常用,博客还是简单讲解一下。Cow不是一个"奶牛指针",它是Copy On Write的缩写,意味写时拷贝。

设想场景,现在有一个已经存在的数据,你不能破坏它的原数据。

你希望用另外一个变量去读取它,需要拷贝这个数据吗?很明显不用,因为读取不会修改数据,你可以直接读取原数据。

你希望在这个数据基础上做一些改进,需要拷贝这个数据吗?那就必须进行一次拷贝,然后在拷贝后的数据上进行修改了,这样才不会破坏原数据。

那么,假设你现在只知道你要读取它,但是不确定未来要不要修改它,你到底是拷贝还是不拷贝呢?万一你拷贝了没修改,那不是白拷贝了?

写时拷贝解决的就是这样一个问题。写时拷贝的策略是:

  1. 对于一份数据,默认以只读的形式使用
  2. 等到需要写入数据时,才把数据拷贝一份出来,再对拷贝后的数据进行修改

这就是写时拷贝,等到写入的时候再进行拷贝。

定义如下:

rust 复制代码
#[stable(feature = "rust1", since = "1.0.0")]
#[rustc_diagnostic_item = "Cow"]
pub enum Cow<'a, B: ?Sized + 'a>
where
    B: ToOwned,
{
    /// Borrowed data.
    #[stable(feature = "rust1", since = "1.0.0")]
    Borrowed(#[stable(feature = "rust1", since = "1.0.0")] &'a B),

    /// Owned data.
    #[stable(feature = "rust1", since = "1.0.0")]
    Owned(#[stable(feature = "rust1", since = "1.0.0")] <B as ToOwned>::Owned),
}

Cow是一个枚举,存在两种形态。Borrow是借用状态,此时稚嫩读取内部的数据BOwned是拥有状态,此时会对数据进行拷贝,后续可以自由写入这个数据。

关于内部更深入的Owned结构就不讲解了,它的重要性还是不如前两者,主要介绍接口。你只要知道它是一个枚举,存在借用和拥有两个状态即可。

由于本身就是携带参数的枚举,构造的方式也很简单,直接报目标借用作为参数传给Cow即可:

rust 复制代码
fn main() {
    let s1: Cow<str> = Cow::Borrowed("Hello, World!");
    let s2: Cow<str> = Cow::Owned(String::from("Hello, Rust!"));

    println!("s1: {}", s1);
    println!("s2: {}", s2);
}

s1是借用形态的Cows2则是所有权形态。

常用方法:

  • as_ref:返回不可变借用
  • to_mut :返回可变借用,如果是Borrow则进行拷贝
  • is_borrowed :判断是否为Borrow形态
  • is_owned :判断是否为Owned形态

后面两个检查形态的方法,需要nightly版本,目前还不稳定。

示例:

rust 复制代码
struct LogString(String);

impl Clone for LogString {
    fn clone(&self) -> Self {
        println!("LogString 被 Clone 了!");
        LogString(self.0.clone())
    }
}

fn main() {
    let borrowed = LogString("hello".into());
    let mut cow: Cow<'_, LogString> = Cow::Borrowed(&borrowed);

    // 读操作,不触发拷贝
    println!("读操作:{}", cow.0);

    // 第一次写入 ------ 写时拷贝触发 Clone!
    println!("写操作");
    cow.to_mut().0.push_str(", world");
    println!("内容:{}", cow.0);
}

这一个被NewType模式包装的String,它重新实现了clone方法,在拷贝时输出"LogString 被 Clone 了!"

随后在main中创建了一个Cow,先对它进行读操作,此时不会触发拷贝。随后进行一次to_mut,并且执行写操作,此时会触发clone输出。

输出结果:

rust 复制代码
读操作:hello
写操作
LogString 被 Clone 了!
内容:hello, world

观察结果,确实是第一次写入时触发的拷贝操作。

Cow也支持Deref,可以直接访问,比如刚才main内部的cow.0就是触发了隐式Deref,因此它也属于智能指针一员。


相关推荐
地平线开发者16 分钟前
征程 6 | cgroup sample
算法·自动驾驶
沛沛老爹18 分钟前
Java泛型擦除:原理、实践与应对策略
java·开发语言·人工智能·企业开发·发展趋势·技术原理
专注_每天进步一点点19 分钟前
【java开发】写接口文档的札记
java·开发语言
代码方舟22 分钟前
Java企业级实战:对接天远名下车辆数量查询API构建自动化风控中台
java·大数据·开发语言·自动化
flysh0524 分钟前
C# 中类型转换与模式匹配核心概念
开发语言·c#
AC赳赳老秦24 分钟前
Python 爬虫进阶:DeepSeek 优化反爬策略与动态数据解析逻辑
开发语言·hadoop·spring boot·爬虫·python·postgresql·deepseek
浩瀚之水_csdn25 分钟前
Python 三元运算符详解
开发语言·python
源代码•宸1 小时前
GoLang八股(Go语言基础)
开发语言·后端·golang·map·defer·recover·panic
姓蔡小朋友1 小时前
算法-滑动窗口
算法
rit84324991 小时前
基于MATLAB的SUSAN特征检测算子边缘提取实现
开发语言·matlab