[CLR via C#]16. 数组

  数组是允许将多个数据项当作一个集合来处理的机制。CLR支持一维数组、多维数组和交错数据(即由数组构成的数组)。所有数组类型都隐式地从System.Array抽象类派生,后者又派生自System.Object。这意味着数组始终是引用类型,是在托管堆上分配的。在你应用程序的变量或字段中,包含的是对数组的引用,而不是包含数组本身的元素。下面的代码更清楚的说明了这一点:

Int32[] myIntegers; //声明一个数组引用
myIntegers = new int32[100] //创建含有100个Int32的数组

  在第一行代码中,myIntegers变量能指向一个一维数组(由Int32值构成)。myIntegers刚开始被设为null,因为当时还没有分配数组。第二行代码分配了含有100个Int32值的一个数组,所有Int32都被初始化为0。由于数组是引 用类型,所有托管堆上还包含一个未装箱Int32所需要的内存块。实际上,除了数组元素,数字对象占据的内存块还包含一个类型对象指针、一个同步块索引和一些额外的成员(overhead)。该数组的内存块地址被返回并保存到myIntegers变量中。

  C#也支持多维数组。下面演示了几个多维数组的例子:

// 创建一个二维数组,由Double值构成
Double[,] myDoubles = new Double[10,20];
// 创建一个三位数组,由String引用构成
String[,,] myStrings = new String[5,3,10]; 

  CLR还支持交错数组,即由数组构成的数组。下面例子演示了如何创建一个多边形数组,其中每一个多边形都由一个Point实例数组构成。

// 创建一个含有Point数组的一维数组
Point[][] myPolygons = new Point[3][];
// myPolygons[0]引用一个含有10个Point实例的数组
myPolygons[0] = new Point[10];
// myPolygons[1]引用一个含有20个Point实例的数组
myPolygons[1] = new Point[20];
// myPolygons[2]引用一个含有30个Point实例的数组
myPolygons[2] = new Point[30];
// 显示第一个多边形中的Point
for (Int32 x =0 ; x < myPolygons[0].Length; x++)
{Console.WriteLine(myPolygons[0][x]);
}

  注意:CLR会验证数组索引的有效性。换句话说,不能创建一个含有100个元素的数组(索引编号为0到99),又试图访问索引为-5或100的元素。

一、始化数组元素

  前面展示了如何创建一个数组对象,以及如何初始化数组中的元素。C#允许用一个语句来同时做两件事。例如:

String[] names = new String[] { "Aidan", "Grant" };

  大括号中的以逗号分隔的数据成为数组初始化器。每个数据项都可以是一个任意复杂度的表达式;在多维数组的情况下,则可以是一个嵌套的数组初始化器。可利用C#的隐式类型的数组功能让编译器推断数组元素的类型。注意,下面这一行代码没有在new和[]之间指定类型:

var names = new[] { "Aidan", "Grant", null};

  在上一行中,编译器检查数组中用于初始化数组元素的表达式的类型,并选择所有元素最接近的共同基类作为数组的类型。在本例中,编译器发现两个String和一个null。由于null可隐式转型成为任意引用类型(包括String),所以编译器推断应该创建和初始化一个由String引用构成的数组。

给定一下代码:

var names = new[] { "Aidan", "Grant", 123};

  编译器是会报错的,虽然String类和Int32共同基类是Object,意味着编译器不得不创建Object引用了一个数组,然后对123进行装箱,并让最后一个数组元素引用已装箱的,值为123的一个Int32。但C#团队认为,隐式对数组 元素进行装箱是一个代价昂贵的操作,所以要做编译时报错。

  在C#中还可以这样初始化数组:

String[] names = { "Aidan", "Grant" };

  但是C#不允许在这种语法中使用隐式类型的局部变量:

var names = { "Aidan", "Grant" };

  最后来看下"隐式类型的数组"如何与"匿名类型"和"隐式类型的局部变量"组合使用。

// 使用C#的隐式类型的局部变量、隐式类型的数组和匿名类型
var kids = new[] {new { Name="Aidan" }, new { Name="Grant" }};
// 示例用法
foreach (var kid in kids)Console.WriteLine(kid.Name);

输出结果:

Aidan
Grant

二、数组转型
  对于元素为引用类型的数组,CLR允许将数组元素从一种类型隐式转型到另一种类型。为了成功转型,两个数组类型必须维数相等,而且从源类型到目标类型,必须存在一个隐式或显示转换。CLR不允许将值类型元素的数组转型为其他任何类型。(不过为了模拟实现这种效果,可利用Array.Copy方法创建一个新数组并在其中填充数据)。下面演示了数组转型过程:

private static void ArrayCasting() {
// 创建一个二维FileStream数组
FileStream[,] fs2dim = new FileStream[5, 10];// 隐式转型为一个二维Object数组
Object[,] o2dim = fs2dim;// 不能从二维数组转型为一维数组
//Stream[] s1dim = (Stream[]) o2dim;// 显式转型为二维Stream数组
Stream[,] s2dim = (Stream[,]) o2dim;// 显式转型为二维String数组
// 能通过编译,但在运行时会抛出异常
String[,] st2dim = (String[,]) o2dim;// 创建一个意味Int32数组(元素是值类型)
Int32[] i1dim = new Int32[5];// 不能将值类型的数组转型为其他任何类型
// Object[] o1dim = (Object[]) i1dim;// 创建一个新数组,使用Array.Copy将元数组中的每一个元素
// 转型为目标数组中的元素类型,并把它们复制过去
// 下面的代码创建一个元素为引用类型的数组,
// 每个元素都是对已装箱的Int32的引用
Object[] o1dim = new Object[i1dim.Length];
Array.Copy(i1dim, o1dim, 0);
}

  Array.Copy方法的作用不仅仅是将元素从一个数组复制到另一个数组。Copy方法还能正确处理内存的重叠区域。 

Copy方法还能在复制每一个数组元素时进行必要的类型转换。Copy方法能执行以下转换:
1)将值类型的元素装箱为引用类型的元素,比如将一个Int32[]复制到一个Object[]中。
2)将引用类型的元素拆箱为值类型的元素,比如将一个Object[]复制到Int32[]中。
3)加宽CLR基元值类型,比如将一个Int32[]的元素复制到一个Double[]中。
4)在两个数组之间复制时,如果仅从数组类型证明不了两者的兼容性。

  在某些情况下,将数组从一种类型转换为另一种类型是非常有用的。这种功能称为数据协变性。利用数组协变性时,应该清楚由此带来的性能损失。

  注意:如果只需要把数组中某些元素复制到另一个数组,可以选择System.Buffer的BlockCopy方法,它的执行速度比Array.Copy方法快。不过,Buffer的BlockCopy方法只支持基元类型,不提供像Array的Copy方法那样的转型能力。方法的Int32参数代表的是数组中的字节偏移量,而非元素索引。如果需要可靠的将一个数组中的元素复制到另一个数组,应该使用System.Array的ConstrainedCopy方法,该方法能保证不破坏目标数组中的数组的前提下完成复制,或者抛出异常。另外,它不执行任何装箱、拆箱或向下类型转换。


三、所有数组都隐式派生自System.Array

  如果像下面这样声明一个数组变量:

FileStream[] fsArray;

  CLR会为AppDomain自动创建一个FileStream[]类型。这个类型将隐式派生自System.Array类型;因此,System.Array类型定义的所有实例方法和属性都将有FileStream[]继承,使这些方法和属性能通过fsArray变量调用。

四、所有数组都隐式实现IEnumerable,ICollection和IList

  许多方法都能操作各种集合对象,因为在声明它们时,使用了IEnumerable,ICollection和IList等参数。可以将数组传给这些方法,因为System.Array也实现了这三个接口。System.Array之所以实现这些非泛型接口,是因为这些接口将所有元素都视为Systm.Object。然而,最好让System.Array实现这个接口的泛型形式,提供更好的编译时类型安全性和更好的性能。

五、数组的传递和返回
  数组作为实参传给一个方法时,实际传递的是对该数组的引用。因此,被调用的方法能修改数组中的元素。如果不想被修改,必须生成数组的一个拷贝,并将这个拷贝传给方法。注意,Array.Copy方法执行的是浅拷贝。

  有的方法返回一个对数组的引用。如果方法构造并初始化数组,返回数组引用是没有问题的。但假如方法返回的是对一个字段维护的内部数组的引用,就必须决定是否向让该方法的调用者直接访问这个数组及其元素。如果是就可以返回数组引用。但是通常情况下,你并不希望方法的调用这获得这个访问权限。所以,方法应该构造一个新数组,并调用Array.Copy返回对新数组的一个引用。

  如果定义一个返回数组引用的方法,而且该数组不包含元素,那么方法既可以返回null,又可以放回对包含另个元素的一个数组的引用。实现这种方法时,Microsoft强烈建议让它返回后者,因为这样做能简化调用该方法时需要的代码。

// 这段代码更容易写,更容易理解
Appointment[] app = GetAppointmentForToday();
for (Int32 a =0; a< app.Length; a++) {
// 对app[a]执行操作
}

如果返回null的话:

// 写起来麻烦,不容易理解
Appointment[] app = GetAppointmentForToday();
if( app !=null ) {
for (Int32 a =0; a< app.Length; a++) {
// 对app[a]执行操作}
}

六、创建下限非零的数组

  可以调用数组的静态CreateInstance方法来动态创建自己的数组。该方法有若干个重载版本,允许指定数组元素的类型、数组的维数、每一维的下限和每一维的元素数目。CreateInstance为数组分配内存,将参数信息保存到数组的内存块的额外开销(overhead)部分。然后返回对该数组的一个引用。

七、数组的访问性能
  CLR内部实际支持两种不同的数组
  1)下限为0的意味数组。这些数组有时称为SZ数组或向量。
  2)下限未知的一维或多维数组。
  可执行一下代码来实际地查看不同种类的输出

internal static class ArrayTypes {
public static void Go() {
Array a;
// 创建一个一维数组的0基数组,其中不包含任何元素 a = new String[0];Console.WriteLine(a.GetType()); // System.String[]// 创建一个一维数组的0基数组,其中不包含任何元素 a = Array.CreateInstance(typeof(String), new Int32[] { 0 }, new Int32[] { 0 });Console.WriteLine(a.GetType()); // System.String[]// 创建一个一维数组的1基数组,其中不包含任何元素 a = Array.CreateInstance(typeof(String), new Int32[] { 0 }, new Int32[] { 1 });Console.WriteLine(a.GetType()); // System.String[*] <-- 注意! Console.WriteLine();// 创建一个二维数组的0基数组,其中不包含任何元素 a = new String[0, 0];Console.WriteLine(a.GetType()); // System.String[,]// 创建一个二维数组的0基数组,其中不包含任何元素 a = Array.CreateInstance(typeof(String), new Int32[] { 0, 0 }, new Int32[] { 0, 0 });Console.WriteLine(a.GetType()); // System.String[,]// 创建一个二维数组的1基数组,其中不包含任何元素 a = Array.CreateInstance(typeof(String), new Int32[] { 0, 0 }, new Int32[] { 1, 1 });Console.WriteLine(a.GetType()); // System.String[,]} }

  对于一维数组,0基数组显示的类型名称是System.String[],但1基数组显示的是System.String[*]。*符号表示CLR知道该数组不是0基的。注意,C#不允许声明String[*]类型的变量,因此不能使用C#语法来访问一维的非0基数组。尽管可以调用Array的GetValue和SetValue方法来访问数组的元素,但速度会比较慢,毕竟有方法调用的开销。

  对于多维数组,0基和1基数组会显示同样的类型名称:System.String[,]。在运行时,CLR将对所有多维数组都视为非0基数组。这自然会人觉得应该显示为System.String[*,*]。但是,对于多维数组,CLR决定不用*符号,避免开发人员对*产生混淆。
  访问一维0基数组的元素比访问非0基数组或多维数组的元素稍快一些。首先,有一些特殊的IL指令,比如newarr,ldelem,ldelema等用于处理一维0基数组,这些特殊IL指令会导致JIT编译器生成优化代码。其次,JIT编译器知道for循环要反问0到Length-1之间的数组元素。所以,JIT编译器生成的代码会在运行时测试所有数组元素的访问都在数组有效访问内。

  如果很关系性能,请考虑由数组构成的数组(即交错数组)来替代矩形数组。

  下面C#代码演示了访问二维数组的三种方式:

internal static class MultiDimArrayPerformance
{
private const Int32 c_numElements = 10000;public static void Go()
{
const Int32 testCount = 10;
Stopwatch sw;// 声明一个二维数组
Int32[,] a2Dim = new Int32[c_numElements, c_numElements];// 将一个二维数组声明为交错数组
Int32[][] aJagged = new Int32[c_numElements][];
for (Int32 x = 0; x < c_numElements; x++)
aJagged[x] = new Int32[c_numElements];// 1: 用普通的安全技术访问数组中的所有元素
sw = Stopwatch.StartNew();
for (Int32 test = 0; test < testCount; test++)
Safe2DimArrayAccess(a2Dim);
Console.WriteLine("{0}: Safe2DimArrayAccess", sw.Elapsed);// 2: 用交错数组技术访问数组中的所有元素
sw = Stopwatch.StartNew();
for (Int32 test = 0; test < testCount; test++)
SafeJaggedArrayAccess(aJagged);
Console.WriteLine("{0}: SafeJaggedArrayAccess", sw.Elapsed);// 3: 用unsafe访问数组中的所有元素
sw = Stopwatch.StartNew();
for (Int32 test = 0; test < testCount; test++)
Unsafe2DimArrayAccess(a2Dim);
Console.WriteLine("{0}: Unsafe2DimArrayAccess", sw.Elapsed);
Console.ReadLine();
}private static Int32 Safe2DimArrayAccess(Int32[,] a)
{Int32 sum = 0;for (Int32 x = 0; x < c_numElements; x++){for (Int32 y = 0; y < c_numElements; y++){sum += a[x, y];}}return sum;
}private static Int32 SafeJaggedArrayAccess(Int32[][] a)
{Int32 sum = 0;for (Int32 x = 0; x < c_numElements; x++){for (Int32 y = 0; y < c_numElements; y++){sum += a[x][y];}
}
return sum;
}private static unsafe Int32 Unsafe2DimArrayAccess(Int32[,] a)
{Int32 sum = 0;fixed (Int32* pi = a){for (Int32 x = 0; x < c_numElements; x++){Int32 baseOfDim = x * c_numElements;for (Int32 y = 0; y < c_numElements; y++){sum += pi[baseOfDim + y];}}}return sum;
}
}

  本机结果是:

  可以看出,安全二维数组访问技术最慢。安全交错数组访问时间略少于安全二维数组。不过应该注意的是:创建交错数组所花的时间多于创建多维数组所花的时间,因为创建交错数组时,要求在堆上为每一维分配一个对象,造成垃圾回收器的周期性活动。所以你可以这样权衡:如果需要创建大量"多个维的数组",而不会频繁访问它的元素,那么创建多维数组就要快点。如果"多个维的数组"只需创建一次,而且要频繁访问它的元素,那么交错数组性能要好点。当然,大多数应用中,后一种情况更常见。

  最后请注意,不安全和安全二维数组访问技术的速度大致相同。但是,考虑到它访问是单个二维数组(产生一次内存分配),二不像交错数组那样需要许多次内存分配。所以它的速度是所有技术中最快的。

八、不安全的数组访问和固定大小的数组

  如果性能是首要目标,请避免在堆上分配托管的数组对象。相反,应该在线程栈上分配数组,这是通过C#的 stackalloc语句来完成的。stackalloc语句只能创建一维0基、由值类型元素构成的数组,而且值类型绝对不能包 含任何引用类型的字段。当然,在栈上分配的内存(数组)会在方法返回时自动释放。

以下代码显示如何使用C#的stackalloc语句:

internal static class StackallocAndInlineArrays
{
public static void Go()
{
StackallocDemo();
InlineArrayDemo();
}private static void StackallocDemo()
{
unsafe
{
const Int32 width = 20;
Char* pc = stackalloc Char[width];    // 在栈上分配数组

String s = "Jeffrey Richter";    // 15 个字符for (Int32 index = 0; index < width; index++)
{
pc[width - index - 1] =
(index < s.Length) ? s[index] : '.';
}
//显示".....rethciR yerffeJ"
Console.WriteLine(new String(pc, 0, width));    
}
}private static void InlineArrayDemo()
{
unsafe
{
CharArray ca;    // 在栈上分配数组
Int32 widthInBytes = sizeof(CharArray);
Int32 width = widthInBytes / 2;String s = "Jeffrey Richter";    // 15 个字符for (Int32 index = 0; index < width; index++)
{
ca.Characters[width - index - 1] =
(index < s.Length) ? s[index] : '.';
}
//显示".....rethciR yerffeJ"
Console.WriteLine(new String(ca.Characters, 0, width));    
}
}private unsafe struct CharArray
{
// 这个数组以内联的方式嵌入结构
public fixed Char Characters[20];
}
}

  通常,因为数组是引用类型,所以在一个结构中定义的数组字段实际只是指向数组的一个指针;数组本身在结构的内存的外部。不过,也可以像上述代码中的CharArray结构那样,直接将数组嵌入结构中。要在结构中直接嵌入一个数组,需要满足以下几个要求:

  1)类型必须是结构(值类型);不能在类(引用类型)中嵌入数组。
  2)字段或其定义结构必须用unsafe关键字标记
  3)数组字段必须使用fixed关键字标记
  4)数组必须是一维0基数组。
  5)数组的元素类型必须是一下类型之一:Boolean,Char,SByte,Byte,Int16,Int32,UInt16,UInt32,Int64,UInt64,Single或Double。
  内联(内嵌)数组常用于和非托管代码进行互操作,而且非托管数据结构也有一个内联数组。不过,也可用于其他情况。

转载于:https://www.cnblogs.com/zxj159/p/3569500.html

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

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

相关文章

Java中String类 compareTo()方法比较字符串详解

中心&#xff1a;String 是字符串,它的比较用compareTo方法,它从第一位开始比较, 如果遇到不同的字符,则马上返回这两个字符的ascii值差值.返回值是int类型1.当两个比较的字符串是英文且长度不等时&#xff0c;1&#xff09;长度短的与长度长的字符一样&#xff0c;则返回的结果…

UIPopoverController简介

1, performSegueWithIdentifier:sender&#xff1a;跳转或弹出控制器 Identifier为popoverSegue时候&#xff0c;Sender仅限于UIBarbuttonItem与View&#xff1b;//。。。。。。。。。 转载于:https://www.cnblogs.com/senlinwuran/p/UIPopoverController.html

异常是catch还是throws的简单原则

1 .如果你完全能处理这个异常&#xff0c;那么就catch掉 public void test() {try {} catch (Exception e) {}}2. 如果你完全不能处理这个异常&#xff0c;那么就throws掉 public void test() throws Exception {}3. 如果你想对异常做一点点处理&#xff0c;但又不能完全处理&a…

在Windows 7 x64 上编译libsvn

这几天由于工作需要&#xff0c;需要Windows上Python 2.7 x64对应的svn模块。Win32版本可以从这个页面直接下载 http://sourceforge.net/projects/win32svnx64的无奈只有自己编译了。在这个过程中还是费了一些力气&#xff0c;在Linux上可以直接make&#xff0c;在Windows使用V…

程序员简历怎么写

说到程序员简历&#xff0c;这两个月&#xff0c;我看过不下10,000份简历。。。 答主不是HR&#xff0c;也不是技术负责人&#xff0c;但是在网站的运营工作中&#xff0c;每天最开心的事情就是研究候选人的简历了~~ 这些人中&#xff0c;有BAT的资深大牛程序员&#xff0c;也有…

android之修改CheckBox左侧图标样式

很多时候系统自带的CheckBox样式并不能满足我们的需求&#xff0c;本文讲解如何替换CheckBox选中&#xff0c;未选中状态下的左侧图片背景的替换。 1.在res目录下创建drawable文件夹&#xff0c;在drawable创建my_checkbox.xml文件 my_checkbox.xml文件内容如下&#xff1a; &l…

步步高DVD机DV603的U盘模式支持视频格式

亲测支持视频格式&#xff1a;支持avi,mpg,vob文件 转载于:https://www.cnblogs.com/phyking/p/4456602.html

SSM框架学习整理

一、Spring原理&#xff1a; 1:核心技术 Spring的两大技术要点便是&#xff0c;一个AOP(面向切面编程)&#xff0c;一个IOC&#xff08;控制反转&#xff09;&#xff0c;而AOP是什么呢&#xff0c;就好比从c语言面向过程编程—>java面向对象编程—>Spring面向切面编程…

安卓手机关闭底部键盘灯的方法(htc G11亲测有效)

还在因为看电子书和看电影时键盘灯刺眼而苦恼吗&#xff1f;下面提供一个方法关闭键盘灯&#xff0c;让你轻松DIY&#xff01;1、手机必须先Root。使用RE管理器&#xff0c;按照这个路径&#xff0c;找到文件&#xff1a;brightnesssys/devices/platform/leds-pm8058/leds/butt…

SSM框架——详细整合教程(Spring+SpringMVC+MyBatis)

使用SSM&#xff08;Spring、SpringMVC和Mybatis&#xff09;已经有三个多月了&#xff0c;项目在技术上已经没有什么难点了&#xff0c;基于现有的技术就可以实现想要的功能&#xff0c;当然肯定有很多可以改进的地方。之前没有记录SSM整合的过程&#xff0c;这次刚刚好基于自…

java基础知识 多线程

package org.base.practise9; import org.junit.Test; import java.awt.event.WindowAdapter; import java.awt.event.WindowEvent; /** * Created with IntelliJ IDEA. * User: cutter.li * Date: 14-3-11 * Time: 上午9:40 * 多线程基础知识练习 */ public class Pract…

最新版的SSM框架spring5.0搭建教程(附源码)

<p>用SSM框架已经有一段时间了&#xff0c;但都没有完整地搭建过一次工程。前段时间比较有时间就自己试着搭建了一下&#xff0c;差不多都是用的最新的spring版本了&#xff0c;然后就在这个基础上做了很多的实验测试。最近想着还是记录一下整个搭建的过程&#xff0c;以…

node.js 针对不同的请求路径(url) 做出不同的响应

边看这个边写的: http://wenku.baidu.com/link?urlC4yLe-TVH6060u_x4t34H3Ze8tjoL7HjJaKgH-TvHnEYl-T_gAMYwhmrCeM0Ji59WBPSkoEXPTWk8dPIZVpbFg_by_gN6DJNGYfjlFuYxE_ 上篇文章讲到了浏览器中访问 http://127.0.0.1:8888/ 输出 "hello world", 但是实际当中, 用户访…

MyBatis 为什么需要通用 Mapper ?

版权声明&#xff1a;版权归博主所有&#xff0c;转载请带上本文链接&#xff01;联系方式&#xff1a;abel533gmail.com https://blog.csdn.net/isea533/article/details/83045335 </div>在早期项目文档中有过类似主题的内容…

Oracle教程-安装、结构(一)

本文安装的是Oracle中的11G版本一、 将文件win32_11gR2_database_1of2.zip和win32_11gR2_database_2of2.zip解压。注意&#xff1a;这两个文件解压到同一个目录下&#xff0c;即&#xff1a;将Components目录合并到一起二、 双击“setup.exe”&#xff0c;弹出以下安装向导。去…

SpringBoot视频教程

SpringBoot视频教程 百度云 置顶 2018年08月02日 11:56:26 SoXiaTea 阅读数 8811 SpringBoot视频教程 百度云 史上最全最精辟的SpringBoot视频教程 B站视频地址 https://www.bilibili.com/video/av33985898 百度云保存地址 全网最实用1.5版本SpringBoot教程 链接&#xf…

[041] 微信公众帐号开发教程第17篇-应用实例之智能翻译

内容概要 本篇文章为大家演示怎样在微信公众帐号上实现“智能翻译”&#xff0c;本例中翻译功能是通过调用“百度翻译API”实现的。智能翻译是指用户随意输入想要翻译的内容&#xff08;单词或句子&#xff09;&#xff0c;系统能自己主动识别用户採用的语言&#xff0c;并将其…

读书,上学,上名校!!!!!

摘自读者上的一篇文章 “龟兔赛跑&#xff0c;如果兔子一直在跑&#xff0c;会发生什么....” 原文作者&#xff1a;王凤 一 念高中时&#xff0c;常听班主任提起一个学姐。她几乎不跟周围的人说话&#xff0c;也没什么朋友&#xff0c;直到高考&#xff0c;她考进全省前10名…

思维模式

人生是可以设计的&#xff0c;生涯是可以规划的&#xff0c;幸福是可以准备的。现在就可以开始。在你穷的时候&#xff0c;要少在家里&#xff0c;多在外面。在你富有的时候&#xff0c;要多在家里&#xff0c;少在外面。这就是生活的艺术。穷得时候&#xff0c;钱要花给别人&a…

钉钉上手体会:阿里实用主义的野望

钉钉出自阿里之手&#xff0c;而阿里是电商出身&#xff0c;在移动办公和协同办公方面不算老司机&#xff0c;但钉钉却凭借阿里的背书声称拿下了这个市场的最大份额&#xff0c;甚至超过后面9名的总和&#xff08;数据来源为钉钉官网发布的《2018中国智能移动办公行业趋势报告》…