一篇文章弄懂C++左值引用和右值引用

目录
  • 1. 左值和右值
  • 2. 左值引用
  • 3. 右值引用
    • 3.1 出现
    • 3.2 概念
    • 3.3 应用
      • 3.3.1 右值引用绑定到左值上
      • 3.3.2 std::move()本质
      • 3.3.3 移动构造函数和移动赋值运算符
      • 3.3.4 std::move()的一个例子
  • 4. 补充—协助完成返回值优化(RVO)
  • 5. 总结

篇幅较长,算是从0开始介绍的,请耐心看~

该篇介绍了左值和右值的区别、左值引用的概念、右值引用的概念、std::move()的本质、移动构造函数、移动复制运算符和RVO。

1. 左值和右值

首先来介绍一下左值和右值的区别,内容参考于《C++ primer 5th》4.1。

当一个对象被用作右值的时候,用的是对象的值(内容);当对象被用作左值的时候,用的是对象的身份(在内存中的位置)。(受对象用途影响)

原则:在需要右值的地方可以用左值代替,但是不能把右值当成左值使用(对象移动除外)。当一个左值代替右值使用时,实际使用的是它的内容(值)。

在C++中,不能单纯的说,左值可以位于赋值语句的左侧,但右值不可以。如:以常量对象为代表的某些左值并不能作为赋值语句的左侧运算对象。例:

const int MAX_LEN = 10; // MAX_LEN是左值
MAX_LEN = 5;            // 错误:试图向const对象赋值。 左值不能位于赋值语句的左侧。

网络上有一种说法,左值位于赋值语句的左侧,右值位于右侧(这句话不是相对于变量说的)。例:

int a = 1;    // a是左值(在内存中的位置), 1是右值(内容),不能给1赋值
int y = a;    // 用左值代替右值,把内容赋值给y。a依然是左值,可以取地址。但是在该表达式中,左值代替右值使用,所以赋值语句右边也可以说是右值,只不过是a的内容。

用到左值的运算符:

  • 赋值运算符需要一个(非常量)左值作为其运算对象,得到的结果也仍然是一个左值。
  • 取地址符作用于一个左值运算对象,返回一个指向该运算对象的指针,这个指针是一个右值(无法放到赋值语句的左侧,进行赋值)。
  • 内置解引用运算符、下标运算符、迭代器解引用运算符、string和vertor的下标运算符。
  • 内置类型和迭代器的递增递减运算符作用于左值运算对象,其前置版本所得的结果也是左值。

总结:常量、有地址的变量一定是左值,临时值是右值。左值可以当成右值用。

2. 左值引用

左值引用:引用是变量的别名,指向左值。但const左值引用除外,由于const的不可变性,所以const引用可以指向右值,我们经常使用const引用作为函数参数传递。例:

int a = 1;
int &b = a;			// 正确
int &c = 10;		// 错误:10是右值。
const int &d = 10;  // 正确

关于左值引用的更多内容,可以参考我的另一篇文章(建议看完左值引用再来看这篇):深入理解左值引用 https://zhuanlan.zhihu.com/p/390611356

3. 右值引用

内容参考于《C++ primer 5th》13.6。

3.1 出现

在重新分配内存的过程中,从旧元素将元素拷贝到新内存是不必要的,更好的方式是移动元素。还有一些可以移动但不能拷贝的类,如:IO类和unique_ptr类。索所以,为了支持移动操作,新标准引入了一种新的引用类型——右值引用。

3.2 概念

右值引用:必须绑定到右值的引用,且只能绑定到一个将要销毁的对象。所以,可以自由地将一个右值引用地资源“移动”到另一个对象中。通过&&获得右值引用(也可以说,接管对象的控制权)。例:

int a = 1;
int &b = a;   			// 正确:左值引用,a是左值
int &&c = a;  			// 错误:右值引用,不能绑定到一个左值上
int &d = a*3; 			// 错误:左值引用,a*3是右值
const int &e = a*3; 	// 正确:左值引用,const引用可以绑定到一个右值上
int &&f = a*3;     		// 正确:右值引用,a*3是右值
int &&g = 10;			// 正确:右值引用,10是右值

变量表达式依然是左值,例:

int &&a = 10;   // 正确:10是右值
int &&b = a; 	// 错误:即使a是右值引用,但a依然是左值,a不是临时对象

从上面的例子,可以看到:左值引用有持久的状态;右值要么是字面常量,要么是在表达式求值过程中创建的临时对象。

由于右值引用只能绑定到临时对象(不管编译器怎么做,但这个我们需要遵守),所以:

所引用的对象将要被销毁

该对象没有其他用户(保证安全,在使用的时候一定要特别确定这一点)

而且,使用右值的代码可以自由地接管所引用的对象的资源。

在移动之后,要谨慎操作原对象,一般不操作,因为我们不确定移动操作做了哪些内容,原对象也是处于一种不确定的状态。

我们一般不会使用const右值引用,当然,编译器也不会报错。(和右值引用的目的冲突)

3.3 应用

3.3.1 右值引用绑定到左值上

在左值与右值的区别中,我们知道左值是可以代替右值的。那么右值引用是不是可以引用到“左值”上呢?答案是可以的,新版标准库给我们提供了一个函数——move(),该函数的含义是:告诉编译器,虽然我们有一个左值,但是我们希望可以像右值一样处理。例:

#include <utility>
int a = 1;
int &&c = a;  			// 错误:右值引用,不能绑定到一个左值上
int &&h = std::move(a); // 正确:使用std::move()把a当成右值处理。

3.3.2 std::move()本质

首先,我们来看一下std::move源码:

// xtr1common文件
	// STRUCT TEMPLATE remove_reference
template<class _Ty>
	struct remove_reference
	{	// remove reference
	using type = _Ty;
	};

// xtr1common文件
template<class _Ty>
	using remove_reference_t = typename remove_reference<_Ty>::type;

// type_traits文件
		// FUNCTION TEMPLATE move
template<class _Ty>
	_NODISCARD constexpr remove_reference_t<_Ty>&&
		move(_Ty&& _Arg) noexcept
	{	// forward _Arg as movable
	return (static_cast<remove_reference_t<_Ty>&&>(_Arg));
	}

从源码中,可以得知:std::move既可以传入一个左值也可以传入一个右值,如果是左值(这里,传入的左值的类型是T&而不是T),则将一个左值转换成右值(_Ty& &&会被折叠成_Ty&, type是T)。其实std::move的作用仅仅是将左值转换成右值,也就是一次类型转换:static_cast<_Ty&&>(_Arg)。也就是说,std::move其实不“移动”,只是转换成右值引用。例:

int v = 5;									// 正确:v是左值
int &&r_ref = 8;							// 正确:re_ref引用右值
int &&r_ref_move = std::move(v); 			// 正确:r_ref_move=5, v是左值,std::move做了一次类型转换。_Ty& &&会被折叠成_Ty&。(赋值之后,v的值是不确定的,这个受移动赋值运算符里的内容影响)
int &&r_ref_move2 = std::move(r_ref_move);	// 正确:r_ref_move2=5, r_ref_move是左值,std::move做了一次类型转换
int &&r_ref_move3 = std::move("hello");		// 正确:可以给一个std::move传递一个右值
v = 9;										// 正确:v、r_ref_move、r_ref_move2=9
r_ref_move = 10;							// 正确:v、r_ref_move、r_ref_move2=10
r_ref_move2 = 11;							// 正确:v、r_ref_move、r_ref_move2=11

3.3.3 移动构造函数和移动赋值运算符

接下来,介绍一下移动构造函数和移动赋值运算符,这两个是右值引用的典型例子。

移动拷贝构造函数

移动构造函数中的第一个参数是该类类型的一个右值引用,本质是在转移对象的控制权。所以我们需要先更新新对象的指针,然后把原对象中的指针置为nullptr。

下面看一个例子:

// 不考虑规范,仅仅是一个例子class MyClass{	// 移动构造函数    // noexcept不抛出异常	MyClass(MyClass &&c) noexcept;	// ...private:	std::string *p;};// 接管c中的内存,不分配任何新内存(与拷贝构造函数不同)MyClass::MyClass(MyClass &&c) noexcept	: p(c.p){	// 对c运行析构函数是安全的(确保原对象进入可析构的状态)	c.p = nullptr;}

移动赋值运算符

移动赋值运算符写法如下:

// 不考虑规范,仅仅是一个例子class MyClass{	// 移动赋值运算符	MyClass& operator=(MyClass &&c) noexcept;	// ...private:	std::string *p;};MyClass& MyClass::operator=(MyClass &&c) noexcept{      // 检查自赋值:不能在使用右侧运算对象的资源之前旧释放左侧运算对象的资源(可能是相同的资源)	if (this != &c) {        free();  //释放已有元素		p = c.p;		c.p = nullptr;	}    return *this;}

关于异常

不抛出异常的移动构造函数和移动赋值运算符必须标记为noexcept。

但是,移动构造函数也可能出现异常,这个时候就不能声明为noexcept。比如:vector的增长,可能会导致内存的重新分配。使用移动构造函数和拷贝构造函数的结果会不同:

  • 如果使用移动构造函数,很有可能移动了部分元素后出现异常,这样会导致——旧空间中的元素已经被改变,新空间中未构造的元素尚不存在。
  • 如果使用拷贝构造函数出现异常,则很容易处理。当在新内存中构造元素时,旧元素保持不变。如果此时发生异常,vector可以释放新分配的内存并返回,vector原有的元素不变。
  • 在重新分配内存的过程中,必须使用拷贝构造函数而不是移动构造函数。(这就是noexcept的作用,让编译器决定是否调用移动构造函数)

合成的移动操作

我们需要注意:如果一个类没有移动操作,类会使用对应的拷贝操作来代替移动操作。编译器可以将一个T&&转换成const T&,然后调用拷贝构造函数。所以,并不是使用了移动就一定可以提升性能。当然,我们可以在自定义类中自己声明定义移动操作。

那么如果我们没有声明定义移动操作,编译器什么时候合成默认的移动函数呢?答案是:一个类没有定义任何自己版本的拷贝控制成员,且类的每个非static数据成员都可以移动时,合成。具体要求如下(忘记出处了,好像是某个翻译过来的…):

  • 如果发生以下情况,编译器将生成移动构造函数(move constructor)

    • 用户未声明拷贝构造函数(copy constructor)
    • 用户未声明拷贝赋值运算符(copy assignment operator)
    • 用户未声明移动赋值运算符(move assignment operator)
    • 用户未声明析构函数(destructor)
    • 该类未被标记为已删除(delete)
    • 所有非static成员均为可移动的(moveable)
  • 如果发生以下情况,编译器将生成移动赋值运算符(move assignment operator)
    • 用户未声明拷贝构造函数(copy constructor)
    • 用户未声明拷贝赋值运算符(copy assignment operator)
    • 用户未声明移动构造函数(move constructor)
    • 用户未声明析构函数(destructor)
    • 该类未被标记为已删除(delete)
    • 所有非static成员均为可移动的(moveable)

而且,移动操作永远不会隐式定义为删除的函数。但是,我们如果我们使用=default显示地要求编译器生成默认移动操作,且编译器不能移动所有成员,编译器会将移动操作定义为删除的函数(安全)。

需要注意的几点:

  • 如果有类成员的移动构造函数或移动赋值运算符被定义为删除的或是不可访问的,则类的移动构造函数或移动赋值运算符被定义为删除的。
  • 如果有类的析构函数被定义为删除的或是不可访问的,则类的移动构造函数被定义为删除的。
  • 如果有类的成员是const的或是引用的,则类的移动赋值运算符被定义为删除的。

定义了一个移动构造函数或移动赋值运算符的类必须也定义自己的拷贝操作。否则,这些成员默认地被定义为删除的。

三/五原则:定义一个类时,建议定义拷贝构造函数、拷贝赋值运算符、析构函数,当需要拷贝资源时,建议也定义移动构造函数、移动赋值运算符。C++并不要求我们定义所有的操作,但是这些操作通常被看成一个整体。

3.3.4 std::move()的一个例子

来源《C++程序设计语言》

来看一下交换函数:

// 一种比较常规的写法template<class T>void swap(T&a, T&b){	T tmp{a};	a = b;	b = tmp;}// 当遇到string、vector这类类型的交换,第一种方法的拷贝将会造成很大的花费,所以出现下面的一种写法:template<class T>void swap(T&a, T&b){	T tmp{static_cast<T&&>(a)};	a = static_cast<T&&>(b);	b = static_cast<T&&>(tmp);}// 由于move函数的本质是static_cast<T&&>,所以对上面的函数还可以优化一下写法template<class T>void swap(T&a, T&b){	T tmp{std::move(a)};	a = std::move(b);	b = std::move(tmp);}

在这个例子中,如果类型T存在移动赋值运算符,那么运算性可能会提高。

4. 补充—协助完成返回值优化(RVO)

来源:《More Effective C++》条款20、《Effective C++》条款21、《C++标准库》3.1.5

例1:

X foo(){    X x;    ...    return x;}

对于例1:

  • 如果X有一个可取用的copy或move构造函数,编译器可以选择略去其中的copy版本,即RVO。(平常简单的返回std::move()可能会出错,这要看优化方式以及编译器怎么处理了)
  • 否则,如果X有一个move构造函数,X就被moved(搬移)。
  • 否则,如果X有一个copy构造函数,X就被copied(复制)。
  • 否则,报出一个编译器错误。

例2:

X&& foo(){    X x;    ...    return std::move(x);}

对于例2,该函数返回的是一个local nonstatic对象,返回右值引用是有风险的。具体看编译器优化。(当然,最好不这样使用。)

例3:

// 对于返回一个对象的函数进行优化。// Rational为分数类,numerator是分子,denominator是分母。// plan 1:返回指针,但是写法很难看(Rational c = *(a*b)),而且可能会导致资源泄露(忘记删除函数返回的指针)。const Rational* operator*(const Rational& lhs, const Rational& rhs);// plan 2:必须付出一个构造函数调用的代价,且可能会导致资源泄露const Rational& operator*(const Rational& lhs, const Rational& rhs){    Rational* result = new Rational(lhs.numerator() * rhs.numerator(), 									lhs.denominator() * rhs.denominator())    return *result;}// plan 3:返回引用,在函数退出前,result已经被销毁。所以,引用指向一个不再存活的对象,会很危险且不正确。const Rational& operator*(const Rational& lhs, const Rational& rhs){    Rational result(lhs.numerator() * rhs.numerator(), 					lhs.denominator() * rhs.denominator())    return result;  // 局部非静态对象}// 所以,如果函数一定得以值方式返回对象,是无法消除的。所以只能尽可能地降低对象返回的成本,而不是想尽办法消除对象本身。// plan 4:有效率且正确的方法。虽然我们构造了临时对象,但是C++允许编译器将临时对象优化,使它们不存在。编译器优化后,调用operator*时没有任何临时对象被调用出来。只需要一个constructor(用以产生c的代价)。const Rational operator*(const Rational& lhs, const Rational& rhs){	return Rational(lhs.numerator() * rhs.numerator(), 					lhs.denominator() * rhs.denominator())}// plan 5:最有效率的做法。使用inline消除调用operator*的函数开销。inline const Rational operator*(const Rational& lhs, const Rational& rhs){	return Rational(lhs.numerator() * rhs.numerator(), 					lhs.denominator() * rhs.denominator())}Rational a = 10;Rational b(1,2);Rational c = a*b;

5. 总结

移动并不移动,只是转移控制权。

std::move()只是做了一次类型转换,转换成一个右值引用,然后方便后续操作,比如:构造、赋值等。真正的内存管理,是交由移动构造、移动赋值等移动操作处理的。有没有性能优化,要看有没有移动操作以及移动操作的处理。

到此这篇关于C++左值引用和右值引用的文章就介绍到这了,更多相关C++左值引用右值引用内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • C++11右值引用和std::move语句实例解析(推荐)

    右值引用(及其支持的Move语意和完美转发)是C++0x将要加入的最重大语言特性之一.从实践角度讲,它能够完美解决C++中长久以来为人所诟病的临时对象效率问题.从语言本身讲,它健全了C++中的引用类型在左值右值方面的缺陷.从库设计者的角度讲,它给库设计者又带来了一把利器.从库使用者的角度讲,不动一兵一卒便可以获得"免费的"效率提升- 下面用实例来深入探讨右值引用. 1.什么是左值,什么是右值,简单说左值可以赋值,右值不可以赋值.以下面代码为例,"A a = getA();&q

  • 浅析C++11中的右值引用、转移语义和完美转发

    1. 左值与右值: C++对于左值和右值没有标准定义,但是有一个被广泛认同的说法:可以取地址的,有名字的,非临时的就是左值;不能取地址的,没有名字的,临时的就是右值. 可见立即数,函数返回的值等都是右值;而非匿名对象(包括变量),函数返回的引用,const对象等都是左值. 从本质上理解,创建和销毁由编译器幕后控制的,程序员只能确保在本行代码有效的,就是右值(包括立即数);而用户创建的,通过作用域规则可知其生存期的,就是左值(包括函数返回的局部变量的引用以及const对象),例如: int& fo

  • C++11右值引用和转发型引用教程详解

    右值引用 为了解决移动语义及完美转发问题,C++11标准引入了右值引用(rvalue reference)这一重要的新概念.右值引用采用T&&这一语法形式,比传统的引用T&(如今被称作左值引用 lvalue reference)多一个&. 如果把经由T&&这一语法形式所产生的引用类型都叫做右值引用,那么这种广义的右值引用又可分为以下三种类型: 无名右值引用 具名右值引用 转发型引用 无名右值引用和具名右值引用的引入主要是为了解决移动语义问题. 转发型引用的引

  • 详解C++11中的右值引用与移动语义

    C++11的一个最主要的特性就是可以移动而非拷贝对象的能力.很多情况都会发生对象的拷贝,有时对象拷贝后就立即销毁,在这些情况下,移动而非拷贝对象会大幅度提升性能. 右值与右值引用 为了支持移动操作,新标准引入了一种新的引用类型--右值引用,就是必须绑定到右值的引用.我们通过&&而不是&来获得右值引用.右值引用一个重要的特性就是只能绑定到将要销毁的对象. 左值和右值是表达式的属性,一些表达式生成或要求左值,而另一些则生成或要求右值.一般而言,一个左值表达式表示的是一个对象的身份,而右

  • C++ lambda 捕获模式与右值引用的使用

    lambda 表达式和右值引用是 C++11 的两个非常有用的特性. lambda 表达式实际上会由编译器创建一个 std::function 对象,以值的方式捕获的变量则会由编译器复制一份,在 std::function 对象中创建一个对应的类型相同的 const 成员变量,如下面的这段代码: int main(){ std::string str = "test"; printf("String address %p in main, str %s\n", &a

  • C++标准之(ravalue reference) 右值引用介绍

    1.右值引用引入的背景 临时对象的产生和拷贝所带来的效率折损,一直是C++所为人诟病的问题.但是C++标准允许编译器对于临时对象的产生具有完全的自由度,从而发展出了CopyElision.RVO(包括NRVO)等编译器优化技术,它们可以防止某些情况下临时对象产生和拷贝.下面简单地介绍一下CopyElision.RVO,对此不感兴趣的可以直接跳过: (1)CopyElision CopyElision技术是为了防止某些不必要的临时对象产生和拷贝,例如: 复制代码 代码如下: structA{ A(

  • 浅谈C++左值引用和右值引用

    实例如下: #include<iostream> #include<utility> #include<vector> using namespace std; int f(); int main() { vector<int>vi(100); int i=42; int &&r1=i;//error不能把右值引用绑到左值上 int &&r2=10; int &r3=i; int &r4=10;//error非

  • C++11的右值引用的具体使用

    C++11 引入了 std::move 语义.右值引用.移动构造和完美转发这些特性.由于这部分篇幅比较长,分为3篇来进行阐述. 在了解这些特性之前,我们先来引入一些问题. 一.问题导入 函数返回值是传值的时候发生几次对象构造.几次拷贝? 函数的形参是值传递的时候发生几次对象构造? 让我们先来看一段代码, // main.cpp #include <iostream> using namespace std; class A{ public: A(){ cout<<"cla

  • 深入解读C++中的右值引用

    右值引用(及其支持的Move语意和完美转发)是C++0x将要加入的最重大语言特性之一,这点从该特性的提案在C++ - State of the Evolution列表上高居榜首也可以看得出来. 从实践角度讲,它能够完美解决C++中长久以来为人所诟病的临时对象效率问题.从语言本身讲,它健全了C++中的引用类型在左值右值方面的缺陷.从库设计者的角度讲,它给库设计者又带来了一把利器.从库使用者的角度讲,不动一兵一卒便可以获得"免费的"效率提升- 在标准C++语言中,临时量(术语为右值,因其出

  • 一篇文章弄懂C++左值引用和右值引用

    目录 1. 左值和右值 2. 左值引用 3. 右值引用 3.1 出现 3.2 概念 3.3 应用 3.3.1 右值引用绑定到左值上 3.3.2 std::move()本质 3.3.3 移动构造函数和移动赋值运算符 3.3.4 std::move()的一个例子 4. 补充-协助完成返回值优化(RVO) 5. 总结 篇幅较长,算是从0开始介绍的,请耐心看~ 该篇介绍了左值和右值的区别.左值引用的概念.右值引用的概念.std::move()的本质.移动构造函数.移动复制运算符和RVO. 1. 左值和右

  • 一篇文章弄懂JVM类加载机制过程以及原理

    目录 一.做一个小测试 二.类的初始化步骤: 三.看看你写对了没? 四.类的加载过程 五.类加载器的分类 1.启动类加载器(引导类加载器) 2.扩展类加载器 3.应用程序类加载器(系统类加载器) 六.类加载器子系统的作用 七.总结 一.做一个小测试 通过注释,标注出下面两个类中每个方法的执行顺序,并写出studentId的最终值. package com.nezha.javase; public class Person1 { private int personId; public Perso

  • 一篇文章弄懂MySQL查询语句的执行过程

    前言 需要从数据库检索某些符合要求的数据,我们很容易写出 Select A B C FROM T WHERE ID = XX  这样的SQL,那么当我们向数据库发送这样一个请求时,数据库到底做了什么? 我们今天以MYSQL为例,揭示一下MySQL数据库的查询过程,并让大家对数据库里的一些零件有所了解. MYSQL架构 mysql架构 MySQL 主要可以分为 Server 层和存储引擎层. Server层 包括连接器.查询缓存.分析器.优化器.执行器等,所有跨存储引擎的功能都在这一层实现,比如存

  • 一篇文章弄懂Java和Kotlin的泛型难点

    Java 和 Kotlin 的泛型算作是一块挺大的知识难点了,涉及到很多很难理解的概念:泛型型参.泛型实参.类型参数.不变.型变.协变.逆变.内联等等.本篇文章就将 Java 和 Kotlin 结合着一起讲,按照我的个人理解来阐述泛型的各个知识难点,希望对你有所帮助

  • 一篇文章弄懂javascript内存泄漏

    1.什么是内存泄漏 在了解什么是内存泄漏之前, 我们应该要对内存是什么有个概念, 随机存取存储器(英语:Random Access Memory,缩写:RAM)是与 CPU 直接交换数据的内部存储器.它可以随时读写, 而且速度很快,通常作为操作系统或其他正在运行中的程序的临时资料存储介质. 什么是内存泄漏? : 程序不再需要使用的内存, 但是又没有及时释放, 就叫做内存泄漏! 然后在理解泄漏之前, 我们的了解下内存的管理, 在一些底层语言中, 如C语言, 内存是需要开发者自己分配和释放的, 通过

  • 一篇文章弄懂Android自定义viewgroup的相关难点

    本文的目的 目的在于教会大家到底如何自定义viewgroup,自定义布局和自定义测量到底如何写.很多网上随便搜搜的概念和流程图 这里不再过多描述了,建议大家看本文之前,先看看基本的自定义viewgroup流程,心中有个大概即可.本文注重于实践 viewgroup 的测量布局流程基本梳理 稍微回顾下,基本的viewgroup绘制和布局流程中的重点: 1.view 在onMeasure()方法中进行自我测量和保存,也就是说对于view(不是viewgroup噢)来说一定在onMeasure方法中计算

  • 一篇文章弄懂javascript中的执行栈与执行上下文

    前言 作为一个前端开发人员,弄清楚JavaScript的执行上下文有助于我们理解js中一些晦涩的概念,比如闭包,作用域,变量提升等等. 执行栈 执行栈用于存储代码执行期间创建的所有执行上下文.具有FILO接口,也被称为调用栈. 当JavaScript代码被运行的时候,会创建一个全局上下文,并push到当前执行栈.之后当发生函数调用的时候,引擎会为函数创建一个函数执行上下文并push到栈顶.引擎会先执行调用栈顶部的函数,当函数执行完成后,当前函数的执行上下文会被移除当前执行栈.并移动到下一个上下文

  • 一篇文章弄懂C#中的async和await

    目录 前言 async await 从以往知识推导 创建异步任务 创建异步任务并返回Task 异步改同步 说说 await Task 说说 async Task<TResult> 同步异步? Task封装异步任务 关于跳到 await 变异步 为什么出现一层层的 await 总结 前言 本文介绍async/Task.在学习该知识点过程中,一定要按照每一个示例,去写代码.执行.输出结果,自己尝试分析思路. async 微软文档:使用 async 修饰符可将方法.lambda 表达式或匿名方法指定

  • 一篇文章弄懂js中的typeof用法

    目录 基础 返回类型 string 和 boolean number 和 bigint symbol undefined function object 其他 常见问题 引用错误 typeof null typeof 的局限性 扩展:BigInt 类型 总结 基础 typeof 运算符是 javascript 的基础知识点,尽管它存在一定的局限性(见下文),但在前端js的实际编码过程中,仍然是使用比较多的类型判断方式. 因此,掌握该运算符的特点,对于写出好的代码,就会起到很大的帮助作用. typ

随机推荐