匹夫细说C#:庖丁解牛迭代器,那些藏在幕后的秘密

匹夫细说C#:庖丁解牛迭代器,那些藏在幕后的秘密

回到目录

0x00 前言

在匹夫的上一篇文章《匹夫细说C#:不是“栈类型”的值类型,从生命周期聊存储位置》的最后,匹夫以总结和后记的方式涉及到一部分迭代器的知识。但是觉得还是不够过瘾,很多需要说清楚的内容还是含糊不清,所以这周就专门写一下c#中的迭代器吧。

回到目录

0x01 你好,迭代器

首先思考一下,在什么情景下我们需要使用到迭代器?

假设我们有一个数据容器(可能是Array,List,Tree等等),对我们这些使用者来说,我们显然希望这个数据容器能提供一种无需了解它的内部实现就可以获取其元素的方法,无论它是Array还是List或者别的什么,我们希望可以通过相同的方法达到我们的目的。

此时,迭代器模式(iterator pattern)便应运而生,它通过持有迭代状态,追踪当前元素并且识别下一个需要被迭代的元素,从而可以让使用者透过特定的界面巡访容器中的每一个元素而不用了解底层的实现。

那么,在c#中,迭代器到底是以一个怎样的面目出现的呢?

如我们所知,它们被封装在IEnumerable和IEnumerator这两个接口中(当然,还有它们的泛型形式,要注意的是泛型形式显然是强类型的。且IEnumerator<T>实现了IDisposable接口)。

IEnumerable非泛型形式:

复制代码
//IEnumerable非泛型形式[ComVisibleAttribute(True)]
[GuidAttribute("496B0ABE-CDEE-11d3-88E8-00902754C43A")]
public interface IEnumerable
{IEnumerator GetEnumerator();
}
复制代码

IEnumerator非泛型形式:

复制代码
//IEnumerator非泛型形式
[ComVisibleAttribute(true)]
[GuidAttribute("496B0ABF-CDEE-11d3-88E8-00902754C43A")]
public interface IEnumerator
{Object Current {get;}bool MoveNext();void Reset();
}
复制代码

IEnumerable泛型形式:

//IEnumerable泛型形式public interface IEnumerable<out T> : IEnumerable
{IEnumerator<T> GetEnumerator();IEnumerator GetEnumerator(); 
}

IEnumerator泛型形式:

复制代码
//IEnumerator泛型形式public interface IEnumerator<out T> : IDisposable, IEnumerator
{void Dispose(); Object Current {get;} T Current {get;}bool MoveNext(); void Reset(); 
}[ComVisibleAttribute(true)]
public interface IDisposable
{void Dispose();
}
复制代码

IEnumerable接口定义了一个可以获取IEnumerator的方法——GetEnumerator()。

而IEnumerator则在目标序列上实现循环迭代(使用MoveNext()方法,以及Current属性来实现),直到你不再需要任何数据或者没有数据可以被返回。使用这个接口,可以保证我们能够实现常见的foreach循环。

为什么会有2个接口?

到此,各位看官是否和曾经的匹夫有相同的疑惑呢?那就是为何IEnumerable自己不直接实现MoveNext()方法、提供Current属性呢?为何还需要额外的一个接口IEnumerator来专门做这个工作?

OK,假设有两个不同的迭代器要对同一个序列进行迭代。当然,这种情况很常见,比如我们使用两个嵌套的foreach语句。我们自然希望两者相安无事,不要互相影响彼此。所以自然而然的,我们需要保证这两个独立的迭代状态能够被正确的保存、处理。这也正是IEnumerator要做的工作。而为了不违背单一职责原则,不使IEnumerable拥有过多职责从而陷入分工不明的窘境,所以IEnumerable自己并没有实现MoveNext()方法。

迭代器的执行步骤

为了更直观的了解一个迭代器,匹夫这里提供一个小例子。

复制代码
using System;
using System.Collections.Generic;class Class1
{ static void Main(){foreach (string s in GetEnumerableTest()){Console.WriteLine(s);}}static IEnumerable<string> GetEnumerableTest(){yield return "begin";for (int i=0; i < 10; i++){yield return i.ToString();}yield return "end";}
}
复制代码

输出结果如图:

OK,那么匹夫就给各位捋一下这段代码的执行过程。

  1. Main调用GetEnumerableTest()方法
  2. GetEnumerableTest()方法会为我们创建一个编译器生成的新的类"Class1/'<GetEnumerableTest>c__Iterator0'"(本例中)的实例。注意,此时GetEnumerableTest()方法中,我们自己的代码尚未执行
  3. Main调用MoveNext()方法
  4. 迭代器开始执行,直到它遇到第一个yield return语句。此时迭代器会获取当前的值是“start”,并且返回true以告知此时还有数据
  5. Main使用Current属性以获取数据,并打印出来
  6. Main再次调用MoveNext()方法
  7. 迭代器继续从上次遇到yield return的地方开始执行,并且和之前一样,直到遇到下一个yield return
  8. 迭代器按照这种方式循环,直到MoveNext()方法返回false,以告知此时已经没有数据了

这个例子中迭代器的执行过程,匹夫已经给各位看官简单的描述了一下。但是还有几点需要关注的,匹夫也想提醒各位注意一下。

  • 在第一次调用MoveNext()方法之前,我们自己在GetEnumerableTest中的代码不会执行
  • 之后调用MoveNext()方法时,会从上次暂停(yield return)的地方开始。
  • 编译器会保证GetEnumerableTest方法中的局部变量能够被保留,换句话说,虽然本例中的i是值类型实例,但是它的值其实是被迭代器保存在堆上的,这样才能保证每次调用MoveNext时,它是可用的。这也是匹夫上一篇文章中说迭代器块中的局部变量会被分配在堆上的原因。

好啦,简单总结了一下C#中的迭代器的外观。那么接下来,我们继续向内部前进,来看看迭代器究竟是如何实现的。

回到目录

0x02 原来是状态机呀

上一节我们已经从外部看到了IEnumerable和IEnumerator这两个接口的用法了,但是它们的内部到底是如何实现的呢?两者之间又有何区别呢?

既然要深入迭代器的内部,这就是一个不得不面对的问题。

那么匹夫就写一个小程序,之后再通过反编译的方式,看看在我们自己手动写的代码背后,编译器究竟又给我们做了哪些工作吧。

为了简便起见,这个小程序仅仅实现一个按顺序返回0-9这10个数字的功能。

IEnumerator的内部实现

首先,我们定义一个返回IEnumerator<T>的方法TestIterator()。

复制代码
//IEnumerator<T>测试using System;
using System.Collections;class Test
{static IEnumerator<int> TestIterator(){for (int i = 0; i < 10; i++){yield return i;}}
}
复制代码

接下来,我们看看反编译之后的代码,探查一下编译器到底为我们做了什么吧。

复制代码
internal class Test
{// Methods 注,此时还没有执行任何我们写的代码private static IEnumerator<int> TestIterator(){return new <TestIterator>d__0(0);}// Nested Types 编译器生成的类,用来实现迭代器。    [CompilerGenerated]private sealed class <TestIterator>d__0 : IEnumerator<int>, IEnumerator, IDisposable{// Fields 字段:state和current是默认出现的private int <>1__state;private int <>2__current;public int <i>5__1;//<i>5__1来自我们迭代器块中的局部变量,匹夫上一篇文章中提到过// Methods 构造函数,初始化状态        [DebuggerHidden]public <TestIterator>d__0(int <>1__state){this.<>1__state = <>1__state;}// 几乎所有的逻辑在这里private bool MoveNext(){switch (this.<>1__state){case 0:this.<>1__state = -1;this.<i>5__1 = 0;while (this.<i>5__1 < 10){this.<>2__current = this.<i>5__1;this.<>1__state = 1;return true;Label_0046:this.<>1__state = -1;this.<i>5__1++;}break;case 1:goto Label_0046;}return false;}[DebuggerHidden]void IEnumerator.Reset(){throw new NotSupportedException();}void IDisposable.Dispose(){}// Propertiesint IEnumerator<int>.Current{[DebuggerHidden]get{return this.<>2__current;}}object IEnumerator.Current{[DebuggerHidden]get{return this.<>2__current;}}}
}
复制代码

我们先全面的看一下反编译之后的代码,可以发现几乎所有的逻辑都发生在MoveNext()方法中。那么之后我们再详细介绍下它,现在我们先从上到下把代码捋一遍。

  1. 这段代码给人的第一印象就是命名似乎很不雅观。的确,这种在正常的C#代码中不会出现的命名,在编译器生成的代码中却是常常出现。因为这样就可以避免和已经存在的正常名字发生冲突的可能性。
  2. 调用TestIterator()方法的结果仅仅是调用了<TestIterator>d__0(编译器生成的用来实现迭代器的类)的构造函数。而这个构造函数会设置迭代器的初始状态,此时的参数为0,而构造函数会将0赋值给记录迭代器状态的字段: this.<>1__state = <>1__state;。注意,此时我们自己的代码并没有执行。
  3. <TestIterator>d__0这个类实现了3个接口:IEnumerator<int>, IEnumerator, IDisposable。
  4. IDisposable的实现十分重要。因为foreach语句会在它自己的finally代码块中调用实现了IDisposable接口的迭代器的Dispose方法。
  5. <TestIterator>d__0类有3个字段:<>1__state,<>2__current, <i>5__1。其中,<>1__state私有字段标识迭代器的状态,<>2__current私有字段则追踪当前的值,而<i>5__1共有字段则是我们在迭代器块中定义的局部变量i。
  6. MoveNext()方法的实现则依托与switch语句。根据状态机的状态,执行不同的代码。
  7. 在本例中Dispose方法什么都没有做。
  8. 在IEnumerator和IEnumerator<int>的实现中,Current都是单纯的返回<>2__current的值。

OK,IEnumerator接口我们看完了。下面再来看看另一个接口IEnumerable吧。

IEnumerator VS IEnumerable

依样画葫芦,这次我们仍然是写一个实现按顺序返回0-9这10个数字的功能的小程序,只不过返回类型变为IEnumerable<T>。

复制代码
using System;
using System.Collections.Generic;class Test
{static IEnumerable<int> TestIterator(){for (int i = 0; i < 10; i++){yield return i;}}
}
复制代码

之后,我们同样通过反编译,看看编译器又背着我们做了什么。

复制代码
internal class Test
{private static IEnumerable<int> TestIterator(){return new <TestIterator>d__0(-2);}private sealed class <TestIterator>d__0 : IEnumerable<int>, IEnumerable, IEnumerator<int>, IEnumerator, IDisposable{// Fieldsprivate int <>1__state;private int <>2__current;private int <>l__initialThreadId;public int <count>5__1;public <TestIterator>d__0(int <>1__state){this.<>1__state = <>1__state;this.<>l__initialThreadId = Thread.CurrentThread.ManagedThreadId;}private bool MoveNext(){switch (this.<>1__state){case 0:this.<>1__state = -1;this.<count>5__1 = 0;while (this.<count>5__1 < 10){this.<>2__current = this.<count>5__1;this.<>1__state = 1;return true;Label_0046:this.<>1__state = -1;this.<count>5__1++;}break;case 1:goto Label_0046;}return false;}IEnumerator<int> IEnumerable<int>.GetEnumerator(){if ((Thread.CurrentThread.ManagedThreadId == this.<>l__initialThreadId) && (this.<>1__state == -2)){this.<>1__state = 0;return this;}return new Test.<TestIterator>d__0(0);}IEnumerator IEnumerable.GetEnumerator(){return ((IEnumerable<Int32>) this).GetEnumerator();}void IEnumerator.Reset(){throw new NotSupportedException();}void IDisposable.Dispose(){}int IEnumerator<int>.Current{get{return this.<>2__current;}}object IEnumerator.Current{get{return this.<>2__current;}}}
}
复制代码

看到反编译出的代码,我们就很容易能对比出区别。

  1. <TestIterator>d__0类不仅实现了IEnumerable<int> 接口,而且还实现了IEnumerator<int>接口。
  2. IEnumerator和IEnumerator<int>的实现都和上面一样。IEnumerator的Reset方法会抛出NotSupportedException异常,而IEnumerator和IEnumerator<int>的Current仍旧会返回<>2__current字段的值。
  3. TestIterator()方法调用<TestIterator>d__0类的构造函数时,传入的参数由上面的0变成了-2:“new <TestIterator>d__0(-2);”。也就是说此时的初始状态是-2。
  4. 又多了一个新的私有字段“<>l__initialThreadId”,且会在<TestIterator>d__0的构造函数中被赋值,用来标识创建该实例的线程。
  5. 实现IEnumerable的GetEnumerator方法,在GetEnumerator方法中要么将状态置为0,并返回this:this.<>1__state = 0;return this;要么就返回一个新的<TestIterator>d__0实例,且初始状态置为0:return new Test.<TestIterator>d__0(0);

所以,从这些对比中我们能发现些什么吗?思考一下我们经常使用的一些用法,包括匹夫在上一节中提供的小例子。不错,我们会创建一个IEnumerable<T>的实例,之后一些语句(例如foreach)会去调用GetEnumerator方法获取一个Enumerator<T>的实例,之后迭代数据,最终结束后释放掉迭代器的实例(这一步foreach会帮我们做)。(而最初我们得到的IEnumerable<T>实例,在第一次调用GetEnumerator方法获得了一个Enumerator<T>实例之后就再没有用到了。

而分析IEnumerable的GetEnumerator方法:

复制代码
        IEnumerator<int> IEnumerable<int>.GetEnumerator(){if ((Thread.CurrentThread.ManagedThreadId == this.<>l__initialThreadId) && (this.<>1__state == -2)){this.<>1__state = 0;return this;}return new Test.<TestIterator>d__0(0);}
复制代码

我们可以发现,-2这个状态,也就是此时的初始状态,表明了GetEnumerator()方法还没有执行。而0这个状态,则表明已经准备好了迭代,但是MoveNext()尚未调用过。

当在不同的线程上调用GetEnumerator方法或者是状态不是-2(证明已经不是初始状态了),则GetEnumerator方法会返回一个<TestIterator>d__0类的新实例用来保存不同的状态。

回到目录

0x03 状态管理

OK,我们深入了迭代器的内部,发现了原来它的实现主要依靠的是一个状态机。那么,下面就让匹夫继续和大伙聊聊这个状态机是如何管理状态的。

状态切换

根据Ecma-334标准,也就是c#语言标准的第26.2 Enumerator objects小节,我们可以知道迭代器有4种可能状态:

  1. before状态
  2. running状态
  3. suspended状态
  4. after状态

而其中before状态是作为初始状态出现的。

在我们讨论状态如何切换之前,匹夫还要带领大家回想一下上面提到的,也就是在调用一个使用了迭代器块,返回类型为一个IEnumerator或IEnumerable接口的方法时,这个方法并非立刻执行我们自己写的代码的。而是会创建一个编译器生成的类的实例,之后当调用MoveNext()方法时(当然如果方法的返回类型是IEnumerable,则要先调用GetEnumerator()方法),我们的代码才会开始执行,直到遇到第一个yield return语句或yield break语句,此时会返回一个布尔值来判断迭代是否结束。当下次再调用MoveNext()方法时,我们的方法会继续从上一个yield return语句处开始执行。

为了能够直观的观察状态的切换,下面小匹夫提供一个类似于《深入理解C#》这本书中的例子:

复制代码
class Test
{static IEnumerable<int> TestStateChange(){Console.WriteLine("----我TestStateChange是第一行代码");Console.WriteLine("----我是第一个yield return前的代码");yield return 1;Console.WriteLine("----我是第一个yield return后的代码");Console.WriteLine("----我是第二个yield return前的代码");yield return 2;Console.WriteLine("----我是第二个yield return前的代码");}static void Main(){Console.WriteLine("调用TestStateChange");IEnumerable<int> iteratorable = TestStateChange();Console.WriteLine("调用GetEnumerator");IEnumerator<int> iterator = iteratorable.GetEnumerator();Console.WriteLine("调用MoveNext()");bool hasNext = iterator.MoveNext();Console.WriteLine("是否有数据={0}; Current={1}", hasNext, iterator.Current);Console.WriteLine("第二次调用MoveNext");hasNext = iterator.MoveNext();Console.WriteLine("是否还有数据={0}; Current={1}", hasNext, iterator.Current);Console.WriteLine("第三次调用MoveNext");hasNext = iterator.MoveNext();Console.WriteLine("是否还有数据={0}", hasNext);}
}
复制代码

之后,我们运行这段代码看看结果如何。

可见,代码的执行顺序就是匹夫刚刚总结的那样。那么我们将这段编译后的代码再反编译回C#,看看编译器到底是如何处理这里的状态切换的。

这里我们只关心两个方法,首先是GetEnumerator方法。其次是MoveNext方法。

复制代码
[DebuggerHidden]
IEnumerator<int> IEnumerable<int>.GetEnumerator()
{if ((Environment.CurrentManagedThreadId == this.<>l__initialThreadId) && (this.<>1__state == -2)){this.<>1__state = 0;return this;}return new Test.<TestStateChange>d__0(0);
}
复制代码

看GetEnumerator方法,我们可以发现:

  1. 此时的初始状态是-2。
  2. 不过一旦调用GetEnumerator,则会将状态置为0。也就是状态从最初的-2,在调用过GetEnumerator方法后变成了0。

我们再来看看MoveNext方法。

复制代码
private bool MoveNext()
{switch (this.<>1__state){case 0:this.<>1__state = -1;Console.WriteLine("----我TestStateChange是第一行代码");Console.WriteLine("----我是第一个yield return前的代码");this.<>2__current = 1;this.<>1__state = 1;return true;case 1:this.<>1__state = -1;Console.WriteLine("----我是第一个yield return后的代码");Console.WriteLine("----我是第二个yield return前的代码");this.<>2__current = 2;this.<>1__state = 2;return true;case 2:this.<>1__state = -1;Console.WriteLine("----我是第二个yield return前的代码");break;}return false;
}
复制代码

由于第一次调用MoveNext方法发生在调用GetEnumerator方法之后,所以此时状态已经变成了0。

可以清晰的看到此时从0——>1——>2——>-1这样的状态切换过程。而且还要注意,每个分支中,this.<>1__state都会首先被置为-1:this.<>1__state = -1。之后才会根据不同的阶段赋值不同的值。而这些不同的值也就用来标识代码从哪里恢复执行。

我们再拿之前实现了按顺序返回0-9这10个数字的小程序的状态管理作为例子,来让我们更加深刻的理解迭代器除了刚刚的例子,还有什么手段可以用来实现“当下次再调用MoveNext()方法时,我们的方法会继续从上一个yield return语句处开始执行。”这一个功能的。

复制代码
private bool MoveNext(){switch (this.<>1__state){case 0:this.<>1__state = -1;this.<i>5__1 = 0;while (this.<i>5__1 < 10){this.<>2__current = this.<i>5__1;this.<>1__state = 1;return true;Label_0046:this.<>1__state = -1;this.<i>5__1++;}break;case 1:goto Label_0046;}return false;}
复制代码

如代码中黄色色带标出的语句,不错,此时状态机是靠着goto语句实现半路插入,进而实现了从yield return处继续执行的功能。

好吧,让我们总结一下关于迭代器内部状态机的状态切换:

  • -2状态:只有IEnumerable才有,表明在第一次调用GetEnumerator之前的状态。
  • -1状态:即上文中提到的C#语言标准中规定的Running状态,表明此时迭代器正在执行。当然,也会用于After状态,例如上例中的case 2中,this.<>1__state被赋值为-1,但是此时迭代结束了。
  • 0状态:即上文中提到的Before状态,表明MoveNext()还一次都没有调用过。
  • 正数(1,2,3...),主要用来标识从遇到yield之后,代码从哪里恢复执行。
回到目录

0x04 总结

通过匹夫上文的分析,可以看出迭代器的实现的确十分复杂。不过值得庆幸的是很多工作都由编译器在幕后为我们做好了。那么,本文就到此结束。欢迎大家探讨。

知识共享许可协议
本作品采用知识共享署名-非商业性使用-相同方式共享 2.5 中国大陆许可协议进行许可,我的博客欢迎复制共享,但在同时,希望保留我的署名权陈嘉栋(慕容小匹夫),并且,不得用于商业用途。如您有任何疑问或者授权方面的协商,请给我留言。

来源: <http://www.cnblogs.com/murongxiaopifu/p/4437432.html>
 


来自为知笔记(Wiz)


转载于:https://www.cnblogs.com/zhiGamer/p/6230542.html

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/489295.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

法国公布“人机协同”项目第二批研究计划

来源&#xff1a;法国《航宇防务》/图片来自互联网出处&#xff1a;国防科技要闻近日&#xff0c;法国公布了“人机协同”&#xff08;MMT&#xff09;项目第二批研究计划。MMT项目由法国武器装备总署&#xff08;DGA&#xff09;管理&#xff0c;于2018年3月启动&#xff0c;是…

xp打印服务器win10的系统连接不上,Win10系统访问WinXP系统共享打印机却连接不了的解决方法...

在今天的Win10系统的使用教程中&#xff0c;我们将来学习的是Win10系统访问WinXP系统共享打印机却连接不了的问题。不过&#xff0c;小编得事先说一下&#xff0c;Win10系统是可以直接连接WinXP系统共享打印机的&#xff0c;只是个别用户由于一些原因而出现这个状况&#xff0c…

c++ map初始化同时赋值_Golang入门教程——map篇

点击上方蓝字&#xff0c;和我一起学技术。今天是golang专题的第7篇文章&#xff0c;我们来聊聊golang当中map的用法。map这个数据结构我们经常使用&#xff0c;存储的是key-value的键值对。在C/java当中叫做map&#xff0c;在Python中叫做dict。这些数据结构的名称虽然不尽相同…

知识图谱简史:从1950到2019

来源&#xff1a;专知 地址&#xff1a;http://knowledgegraph.today/paper.html 作者 | Claudio Gutierrez 、 Juan F. Sequeda 编译 | Xiaowen 目录&#xff1a; 知识图谱可以被认为是实现计算机科学的早期愿景&#xff0c;即创建能够大规模集成知识和数据的智能系统。“知识…

影响计算机算法世界的十位大师(上)

来源&#xff1a;数学职业家1、伟大的智者——Don E.Knuth&#xff0c;中文名&#xff1a;高德纳(1938-)算法和程序设计技术的先驱者。Oh,God!一些国外网站这样评价他。一般说来&#xff0c;不知道此人的程序员是不可原谅的。其经典著作《计算机程序设计艺术》更是被誉为算法中…

echars显示折点数据_数据可视化的基础语法

数据可视化的基础语法可视化主要是以图像来展示数据间的关系&#xff0c;常见的图形种类有折线图,散点图,条形图&#xff0c;直方图&#xff0c;饼图。此外在接下来课程中还会用到箱线图&#xff0c;热力图&#xff0c;蜘蛛图&#xff0c;表示二元变量分布和成对关系的视图。今…

cad快捷键文件路径_办公格式转太难不会看这里!CAD、PDF、Word、Excel、TXT教你玩转...

办公格式转&#xff0c;你遇到最难解决的问题是哪些&#xff1f;办公格式转&#xff0c;最让你头疼的文件格式是哪些&#xff1f;办公格式转&#xff0c;你最想要学习转换格式有哪些&#xff1f;我&#xff1a;Word、Excel、PPT、TXT、CAD、PDF、JPG统统都想要进行解决&#xf…

80x86汇编—分支循环程序设计

文章目录 查表法: 实现16进制数转ASCII码显示计算AX的绝对值判断有无实根地址表形成多分支从100,99,...,2,1倒序累加输入一个字符&#xff0c;然后输出它的二进制数大小写转换大写转小写小写转大写 冒泡排序剔除空格 查表法: 实现16进制数转ASCII码显示 题目要求&#xff1a; …

织梦服务器系统win10,WIN服务器爆破DEDECMS后台目录

利用脚本python&#xff1a;#!/usr/bin/env python/** author Mochazz* team 红日安全团队* env pyton3**/import requestsimport itertoolscharacters "abcdefghijklmnopqrstuvwxyz0123456789_!#"back_dir ""flag 0url "http://192.168.1.9/t…

人工智能技术对专利制度的挑战与应对

来源&#xff1a;上海市法学会内容摘要人工智能技术作为人类的一项发明创造&#xff0c;本身具备了一定的创造力。无论是专利法治实践还是理论研究均无法否认&#xff0c;在弱人工智能时代&#xff0c;人工智能技术兼具发明创造工具和发明创造方案的提供者的双重身份。由此导致…

网络推广恶意点击js_做好网络推广,是做好网络营销的必要条件

互联网社会里网络无处不在&#xff0c;商机无处不在&#xff0c;企业通过网络营销&#xff0c;能有效的避免线下竞争激烈的市场&#xff0c;做好网络推广&#xff0c;是做好网络营销的必要条件。一个优秀的网络推广团队不但能实现提高企业的知名度的目的&#xff0c;还能帮助企…

css盒子模型_css的盒子模型是什么

CSS盒子模型就是在网页设计中经常用到的CSS技术所使用的一种思维模型。css盒子模型又称为框模型 (Box Model) &#xff0c;包含了元素内容(content)、内边距(padding)、边框(border)、外边距(margin)几个要素组成了盒子模型。图中最内部的框是元素的实际内容&#xff0c;也就是…

6G应用场景有哪些?首份6G报告给你揭晓

来源&#xff1a; 5G产业圈导 读中国移动研究院无线与终端技术研究所所长丁海煜介绍称&#xff0c;按照移动通信产业“使用一代、建设一代、研发一代”的发展节奏&#xff0c;业界预期6G将于2030年左右实现商用。“创新、协调、绿色、开放、共享”应成为5G向6G演进的新发展理念…

linux python 图形界面开发_python在linux制作图形界面(snack)

snack是一个用于在linux制作图形界面&#xff08;GUI&#xff09;的模块&#xff0c;该模块由c编写&#xff0c;而且redhat的系统都自带这个模块。 1.获取模块 虽然redhat系统会自带这个模块&#xff0c;但是直接去import snack会提示找不到模块&#xff0c;一个原因是我们重装…

虚拟搭建局域网模拟器_巧用虚拟局域网,快速搭建私有云,一步就能搞定

前段时间公司一直在使用局域网来进行文件共享&#xff0c;用一台电脑把某个文件夹在局域网里共享出来&#xff0c;其他电脑远程访问&#xff0c;但是这样使用太麻烦&#xff0c;要是主电脑忘记开机&#xff0c;其它人就用不了。再就是&#xff0c;一旦离开公司之后&#xff0c;…

5个层级带你看清一颗芯片的内部结构

来源&#xff1a;北京物联网智能技术应用协会导 读在我们阐明半导体芯片之前&#xff0c;我们先应该了解两点。其一半导体是什么&#xff0c;其二芯片是什么。

python使用shell命令_python 调用shell命令的方法

在python程序中调用shell命令&#xff0c;是件很酷且常用的事情…… 1. os.system(command) 此函数会启动子进程&#xff0c;在子进程中执行command&#xff0c;并返回command命令执行完毕后的退出状态&#xff0c;如果command有执行内容&#xff0c;会在标准输出显示。这实际上…

java中必检异常有哪些_Java面试题经典面试题220道(附答案)

Java基础&#xff1a;1.JDK 和 JRE 有什么区别&#xff1f;2. 和 equals 的区别是什么&#xff1f; 解读3. 两个对象的 hashCode() 相同&#xff0c; 那么 equals() 也一定为 true吗&#xff1f;4. final 在 Java 中有什么作用&#xff1f;5. Java 中的 Math. round(-1. 5) 等…

11项关键先进制造技术解读!

来源&#xff1a;中国指挥与控制学会图片&#xff1a;来源于网络上届美国总统奥巴马非常重视制造业。其发起成立的“先进制造业合作委员会”&#xff0c;Advanced Manufacturing Partnership&#xff0c;就未来制造业的发展做出了展望&#xff0c;重点规划11个技术领域&#xf…

网站程序数据库怎么上传到服务器上,网站的数据库怎么上传到服务器

网站的数据库怎么上传到服务器 内容精选换一换通常在将数据导入数据库前&#xff0c;即将入库的数据已经在相关主机上了。我们称这种保存着待入库数据的服务器为数据服务器。此时&#xff0c;只需检测以确认数据服务器和GaussDB(for openGauss)集群能够正常通信&#xff0c;并查…