C# 求精简用一行代码完成的多项判断 重复赋值

哈哈,说实话,个人看着这么长的三元操作也麻烦,但是我也只想到了这样三元判断句中执行方法体能够写到一行,追求的终极目的是,用一行实现这个过程,而且简单,由于篇幅,我就不截图我其实是放在一行上

讨论的重点是怎么做的更极致,而不是这样搞规范不规范,求更有创意的写法。。。

。。 。。华丽丽的分割线。。 。。

var turecolor = colorJToken.OfType<JProperty>().Sum(d => (int)d.Value << "bgra".IndexOf(d.Name) * 8).ToString("X8");

如果是处理颜色,相信 Choo给出的写法想必是极好的,那么如果扩展成普通的用法呢?

。。 。。华丽丽的分割线。。 。。

刚说不实用,先不讨论是不是实用,也不管可读性,单纯的想把代码写在一行上,而且写的少,总结一下,主要内容在这,我想到的写到一行的方法是在三元判断句中执行方法体

bool b = 1 == 1 ? ((Func<bool>)(() => true)).Invoke() : ((Func<bool>)(() => false)).Invoke();

能不能继续精简这样一句话?

鬼柒 在评论中提出了直接纯赋值的写法,嗯,这个在不需要做额外判断的时候好了太多,那么现在的场景是,需要进行判断赋值的话,怎么样写呢?

( Name == "a" ? alph = (Byte)Value : Name == "r" ? red = (Byte)Value : Name == "g" ? green = (Byte)Value : Name == "b" ? blue = (Byte)Value).ToString(0);

。。 。。华丽丽的分割线。。 。。

其中 color仅仅为一个普通的JToken类型,需要精简的是,对于循环重复赋值上,能不能有更简单而且简洁的写法。。。

需求是,用一行完成,且必须有能够执行多判断的地方

            Byte red = 0, green = 0, blue = 0, alph = 255;
JToken colorJToken = CurrentVersion < ColorVersion ? color.FirstOrDefault() : color;
if (colorJToken != null)
{
foreach (var item in colorJToken)
{
var colorJProperty = item as JProperty;
if (colorJProperty != null && colorJProperty.Value != null)
{
//以下求精简
(colorJProperty.Name == "a" ? ((Func<bool>)(() => colorJProperty.Value is Byte && (alph = (Byte)colorJProperty.Value).Equals(alph))).Invoke() :
(colorJProperty.Name == "r" ? ((Func<bool>)(() => (red = (Byte)colorJProperty.Value).Equals(red))).Invoke() :
(colorJProperty.Name == "g" ? ((Func<bool>)(() => (green = (Byte)colorJProperty.Value).Equals(green))).Invoke() :
(colorJProperty.Name == "b" ? ((Func<bool>)(() => (blue = (Byte)colorJProperty.Value).Equals(blue))).Invoke() :
((Func<bool>)(() => false)).Invoke())))).ToString();
}
}
}

 
    在上一章中,我们讲解了ConcurrentHashMap的读取效率很高的原因,一般来说并发的读取和写入是一对矛盾体,而缓存的过期移除和持久化则是另一对矛盾体。这一节,我们着重来了解下高并发情况下缓存的写入、过期控制及周边相关功能。
 

1.高效的数据写入(put)

    在研究写入机制之前,我们先来回顾下上一节的内容。ConcurrentHashMap之所以读取很快,很大一部分原因归功于它的数据分割设计,就像是把书的内容划分为很多章,章下面又分了许多小节。同样的原理,写入过程也可以按这个规则把数据分为很多独立的块,也就是前一节提到的Segment。另一方面为了解决并发问题,加锁是一个不错的选择。再回头看看Segment类图(清单1),Segment其实是继承了ReentrantLock,自己本身就是一个锁。
清单1:Segment类图
 
    想要最大限度的支持并发,就是读写操作都不加锁,JDK8 就实现了无锁的并发HashMap,效率更高的同时复杂度也更大;而在锁机制中,一个很好的方法就是读操作不加锁,写操作加锁,对于竞争资源来说就需要定义为volatile类型的。volatile类型能够保证happens-before法则,所以volatile能够近似保证正确性的情况下最大程度的降低加锁带来的影响,同时还与写操作的锁不产生冲突。

“锁分离” 技术

    在竞争激烈的情况下,如果写入时对缓存中所有数据都加锁,效率必然低下,HashTable的效率不高就是因为这个原因。为此ConcurrentHashMap默认把数据分为16个Segment,每个Segment就是一把锁,也就是一个独立的数据块,那么多线程读写不同Segment的数据时就不会存在锁竞争,从而可以有效的提高读写效率,这就是“锁分离”技术。缓存中几乎所有的操作都是基于独立的segment数据块,且在修改时必须对segment加锁。
 
    缓存的put()操作与get()操作类似,得到元素修改就行了,更多的参考源码,这里有几点要注意:
    a.如果对数据做了新增或移除,需要修改count的数值,这个要放到整个操作的最后,为什么?前面说过count是volatile类型,而读取操作没有加锁,所以只能把元素真正写回Segment中的时候才能修改count值,所以这个必须要放到整个操作的最后。
    b.因为HashEntry的next属性是final的,所以后加入的元素都是加在链表的头部
    c.为什么在put操作中首先建立一个临时变量tab指向Segment的table,而不是直接使用table?这是因为table变量是volatile类型,多次读取volatile类型的开销要比非volatile开销要大,而且编译器也无法优化,多次读写tab的效率要比volatile类型的table要高,JVM也能够对此进行优化。    

2.巧妙的数据移除(remove)

    上一节中,我们也提到,为了防止在遍历HashEntry的时候被破坏,HashEntry中除了value之外其他属性都是final常量,否则不可避免的会得到ConcurrentModificationException,这就意味着,不能把节点添加到链表的中间和尾部,也不能在链表的中间和尾部删除节点,只能在头部添加节点。这个特性可以保证:在访问某个节点时,这个节点之后的链表不会被改变。这样可以大大降低处理链表时的复杂性。既然不能改变链表,缓存到底是如何移除对象的呢?我们首先来看下面两幅图:
 
清单2. 执行删除之前的原链表1:
清单3. 执行删除之后的新链表2

    假设现在有一元素C需要删除,根据上一节所讲,C必然存在某一链表1中,假设这个链表结构为A->B->C->D->E,那现在该如何从链表1中删除C元素?
    根据上一节的内容,我们先要经过2次hash定位,即我们先要定位到C所在的Segment,然后再定位到Segment中table的下标(C所在的链表)。然后遍历链表找到C元素,找到之后就把C的next节点D作为临时头节点构成链表2,然后从现有头节点A开始向后迭代加入到链表2的头部,一直到需要删除的C节点结束,即A、B依次都作为临时头节点加入链表2,最后的情形就是A加入到D前面,B又加入到A前面,这样就构造出来一个新的链表B->A->D->E,然后将此链表的最新头节点B设置到Segment的table中。这样就完成了元素C的删除操作。
    需要说明的是,尽管就的链表仍然存在(A->B->C->D->E),但是由于没有引用指向此链表,所以此链表中无引用的(A->B->C)最终会被GC回收掉。这样做的一个好处是,如果某个读操作在删除时已经定位到了旧的链表上,那么此操作仍然将能读到数据,只不过读取到的是旧数据而已,这在多线程里面是没有问题的。
    从上面的流程可以看出,缓存的数据移除不是通过更改节点的next属性,而是通过重新构造一条新的链表来实现的,这样即保证了链条的完整性,同时也保证了并发读取的正确性。源码如下:
 
 清单4:缓存数据的移除
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
 
void removeEntry(HashEntry entry, int hash) {
    int c = count - 1;
    AtomicReferenceArray<HashEntry> tab = table;
    int index = hash & (tab.length() - 1);
    HashEntry first = tab.get(index);

for (HashEntry e = first; e != null; e = e.next) {
        if (e == entry) {
            ++modCount;
            // 从链表1中删除元素entry,且返回链表2的头节点
            HashEntry newFirst = removeEntryFromChain(first, entry);
            // 将链表2的新的头节点设置到segment的table中
            tab.set(index, newFirst);
            count = c; // write-volatile,segment内的元素个数-1
            return;
        }
    }
}

HashEntry removeEntryFromChain(HashEntry first, HashEntry entry) {
    HashEntry newFirst = entry.next;
    // 从链条1的头节点first开始迭代到需要删除的节点entry
    for (HashEntry e = first; e != entry; e = e.next) {
        // 拷贝e的属性,并作为链条2的临时头节点
        newFirst = copyEntry(e, newFirst);
    }
    accessQueue.remove(entry);
    return newFirst;
}

HashEntry copyEntry(HashEntry original, HashEntry newNext) {
    // 属性拷贝
    HashEntry newEntry = new HashEntry(original.getKey(), original.getHash(), newNext, original.value);
    copyAccessEntry(original, newEntry);
    return newEntry;
}

3.按需扩容机制(rehash)

    缓存中构造函数有3个参数与容量相关:initialCapacity代表缓存的初始容量、loadFactor代表负载因子、concurrencyLevel字面上的意思是并发等级,其实就是segment的数量(内部会转变为2的n次方)。既然有初始容量,则自然有容量不足的情况,这种情况就需要对系统扩容,随之而来的就是2个问题:何时扩容以及如何扩容?
    第一个问题:何时扩容,缓存就使用到了loadFactor负载因子,在插入元素前会先判断Segment里的HashEntry数组是否超过阈值threshold = (int) (newTable.length() * loadFactor),如果超过阀值,则调用rehash方法进行扩容。
    那如何扩容呢?扩容的时候首先会创建一个两倍于原容量的数组,然后将原数组里的元素进行再hash后插入到新的数组里。为了效率缓存不会对整个容器进行扩容,而只对某个segment进行扩容。这样对于其他segment的读写都不影响。扩容的本质就是把数据的key按照新的容量重新hash放到新组建的数组中,但是相较于HashMap的扩容,ConcurrentHashMap有了些许改进。
    我们来看个小例子:假设原有数组长度为16,根据上一节知识,我们知道掩码为1111,扩容后新的数组长度为16*2=32,掩码为11111。由下图可以看出扩容前掩码15到扩容后掩码31,也就是粉色那一列的掩码值由0变为1,这样子如果hash蓝色那一列的值原来是0,则扩容后下标和扩容前一样,如果原来是1,则扩容后下标=扩容前下标+16,由此我们可以得出结论:扩容前的长度为length的数组下标为n的元素,映射到扩容后数组的下标为n或n+length。
 
0111|1110 hash二进制 
0000|1111 掩码15
-------------‘与’运算-----------
0000|1110 扩容前数组下标
 
0111|1110 hash二进制 
0001|1111 掩码31
-------------‘与’运算-----------
0001|1110 扩容后数组下标,比扩容前下标大10000,转换为十进制就是16
 
    假设我们有链表A[5] -> B[21] -> C[5] -> D[21] -> E[21] -> F[21],中括号内代表的是扩容后的数组下标。基于上面的原理,ConcurrentHashMap扩容是先把链表后面的一整段连续相同下标的元素链(D[21] -> E[21] -> F[21])找出来,直接复用这个链,然后复制这个链之前的元素(A[5] -> B[21] -> C[5]),这样就避免了所有元素的复制。
    事实上,根据JDK的描述:Statistically, at the default threshold, only about one-sixth of them need cloning when a table double,翻译过来就是:据统计,使用默认的阈值,扩容时仅有1/6的数据需要复制。
 
 清单5:缓存数据的扩容
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
 
void rehash() {
    /**
     * .... 部分代码省略
     */
    for (int oldIndex = 0; oldIndex < oldCapacity; ++oldIndex) {
        HashEntry head = oldTable.get(oldIndex);
        if (head != null) {
            HashEntry next = head.next;
            int headIndex = head.getHash() & newMask;

// next为空代表这个链表就只有一个元素,直接把这个元素设置到新数组中
            if (next == null) {
                newTable.set(headIndex, head);
            } else {
                // 有多个元素时
                HashEntry tail = head;
                int tailIndex = headIndex;
                // 从head开始,一直到链条末尾,找到最后一个下标与head下标不一致的元素
                for (HashEntry e = next; e != null; e = e.next) {
                    int newIndex = e.getHash() & newMask;
                    if (newIndex != tailIndex) { // 这里的找到后没有退出循环,继续找下一个不一致的下标
                        tailIndex = newIndex;
                        tail = e;
                    }
                }
                // 找到的是最后一个不一致的,所以tail往后的都是一致的下标
                newTable.set(tailIndex, tail);

// 在这之前的元素下标有可能一样,也有可能不一样,所以把前面的元素重新复制一遍放到新数组中
                for (HashEntry e = head; e != tail; e = e.next) {
                    int newIndex = e.getHash() & newMask;
                    HashEntry newNext = newTable.get(newIndex);
                    HashEntry newFirst = copyEntry(e, newNext);
                    if (newFirst != null) {
                        newTable.set(newIndex, newFirst);
                    } else {
                        accessQueue.remove(e);
                        newCount--;
                    }
                }
            }
        }
    }
    table = newTable;
    this.count = newCount;
}

    
4.过期机制(expire)
    既然叫做缓存,则必定存在缓存过期的概念。为了提高性能,读写数据时需要自动延长缓存过期时间。又因为我们这里所讲的缓存有持久化操作,则要求数据写入DB之前缓存不能过期。
    数据拥有生命周期,我们可设置缓存的accessTime解决;读写数据时自动延长周期,也就是读写的时候都需要修改缓存的accessTime;那如何保证数据写入DB前不能清除缓存呢?
    一种方法就是定期遍历缓存中所有的元素,检测缓存中数据是否全部写入到库中,如果已写入且达到过期时间,则可移除此缓存。很明显这种方式最大的问题在于需要定期检测所有数据,也就是短期内会有高CPU负载;另一方面,缓存的过期时间不精准,因为缓存的过期是基于定期检测的,不到定期检测时间,缓存就会存在于内存中。

时间轴

    在平时,我们可能会了解到一个名词:timeline,翻译过来就是“时间轴”。请看下面一个简单的示例
 
清单6:时间轴TimeLine
----进入时间最短-----Enter-->--D-->--C-->--B-->--A-->--进入时间最久-----
 
    我们的缓存数据就存在于这个时间轴上,如上例所示:数据A产生或变化后我们可以把它放到时间轴的Enter点,随着时间的推移,B、C、D也都会依次放在Enter点,最终就形成了上面的一个时间轴。当有时间轴上的数据发生变更,我们再把它从时间轴上移除,当做新数据重新加入Enter点。
    那我们如何检测数据有没有过期?因为在时间轴上的数据都是有序的,问题就很简单了,缓存的产生和改变都是有先后顺序的,我们只要找到第一个没过期的元素,则比它进入时间短的数据都是没过期的。整个流程进一步看来就是一个可以删除指定元素的先进先出队列,基于这个原理可实现缓存的过期。

删除指定元素的先进先出队列AccessQueue

    目前存在一种常见做法-双向链表形式的环状队列,在这种队列中的元素提供了获取前一个和后一个元素的引用,新的元素插入到链表的尾端,过期元素从头部过期,而双向链表一个很重要的特点就是它可以很方便的从队列中间移除元素。队列AccessQueue继承了AbstractQueue,拥有了队列的基本功能,队列内的元素都是ReferenceEntry,它有3个子类:Head(队列头)、HashEntry(队列内元素)、NullEntry(主要用于移除队列元素)。其中Head元素是一直存在的,默认其previousAccess和nextAccess都指向head自身。
 
清单7:接口ReferenceEntry
清单8:队列AccessQueue的结构
    请看队列AccessQueue的结构图,这里我们假设队列是按照逆时针添加元素的,则元素0、1、2是依次添加到队列中的。
    数据移除:假设需要移除节点1,需要先把节点1的上个节点0和下个节点2链接起来,然后把节点1的previousAccess和nextAccess都链接到NullEntry,以确保元素1在JVM中无法再被引用,方便被GC回收。
    数据新增:假设需要增加节点1到tail,有可能节点1已经存在于链表中,则需要先把节点1的上个节点0和下个节点2链接起来,然后再添加到尾部。
 
 清单9:可以移除元素的先进先出队列
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
 
static final class AccessQueue extends AbstractQueue<ReferenceEntry> {
    // head代码省略
    final ReferenceEntry head = XXX;
    // 其他部分代码省略

@Override
    public boolean offer(ReferenceEntry entry) {
        // 将上一个节点与下一个节点链接,也就是把entry从链表中移除
        connectAccessOrder(entry.getPreviousInAccessQueue(), entry.getNextInAccessQueue());

// 添加到链表tail
        connectAccessOrder(head.getPreviousInAccessQueue(), entry);
        connectAccessOrder(entry, head);

return true;
    }

@Override
    public ReferenceEntry peek() {
        // 从head开始获取
        ReferenceEntry next = head.getNextInAccessQueue();
        return (next == head) ? null : next;
    }

@Override
    public boolean remove(Object o) {
        ReferenceEntry e = (ReferenceEntry) o;
        ReferenceEntry previous = e.getPreviousInAccessQueue();
        ReferenceEntry next = e.getNextInAccessQueue();
        // 将上一个节点与下一个节点链接
        connectAccessOrder(previous, next);
        // 方便GC回收
        nullifyAccessOrder(e);

return next != NullEntry.INSTANCE;
    }
}

// 将previous与next链接起来
static void connectAccessOrder(ReferenceEntry previous, ReferenceEntry next) {
    previous.setNextInAccessQueue(next);
    next.setPreviousInAccessQueue(previous);
}

// 将nulled的previousAccess和nextAccess都设为nullEntry,方便GC回收nulled
static void nullifyAccessOrder(ReferenceEntry nulled) {
    ReferenceEntry nullEntry = nullEntry();
    nulled.setNextInAccessQueue(nullEntry);
    nulled.setPreviousInAccessQueue(nullEntry);
}

何时进行过期移除?

    在拥有了定制版的先进先出队列,缓存过期就相对比较简单了,我们只要把新增和修改的数据放到队列尾部,然后从队列首部依次判断数据是否过期就可以了。那什么时候去执行这个操作呢?google的缓存是放在每次写入操作或者每64次读操作执行一次清理操作。一方面,因为缓存是不停在使用的,这就决定了过期的缓存不可能累积太多;另一方面,缓存的过期仅仅是时间点的判断,速度非常快。所以这样操作性能并没有带来性能的降低,但是却带来了缓存过期的准确性。
 
 清单10:读操作执行清理 
1
2
3
4
5
6
7
 
void postReadCleanup() {
    // 作为位操作的mask(DRAIN_THRESHOLD),必须是(2^n)-1,也就是1111的二进制格式
    if ((readCount.incrementAndGet() & DRAIN_THRESHOLD) == 0) {
        // 代表每2^n执行一次
        cleanUp();
    }
}
 
 清单11:缓存过期移除 
1
2
3
4
5
6
7
8
9
10
11
 
void expireEntries(long now) {
    drainRecencyQueue();

ReferenceEntry e;
    // 从头部获取,过期且已保存db则移除
    while ((e = accessQueue.peek()) != null && map.isExpired(e, now)) {
        if (e.getValue().isAllPersist()) {
            removeEntry((HashEntry) e, e.getHash());
        }
    }
}

5.个数统计(size)

    前面提到,缓存中数据是分为多个segment的,如果我们要统计缓存的大小,就要统计所有segment的大小后求和,我们是不是直接把所有Segment的count相加就可以得到整个ConcurrentHashMap大小了呢?答案当然是否定的,虽然相加时可以获取每个Segment的count的最新值,但是拿到之后可能累加前使用的count发生了变化,那么统计结果就不准了。那该如何解决这个问题?
    一个方法就是把所有segment都锁定然后求和,很显然这种方法效率非常低下,不可取。因此ConcurrentHashMap里提供了另一种解决方法,就是先尝试2次通过不锁住Segment的方式来统计各个Segment大小,如果统计的过程中,容器的count发生了变化,则再采用加锁的方式来统计所有Segment的大小。
    那么ConcurrentHashMap是如何判断在统计的时候segment是否发生了变化呢?答案是使用modCount变量。每个segment中都有一个modCount变量,代表的是对segment中元素的数量造成影响的操作的次数,这个值只增不减。有了这个它就可以判定segment是否变化了。
    所以,size操作本质上就是两次循环尝试,失败了则锁定获取,这种类似无锁的操作方式对性能是有很大的提升,因为大部分情况下两次循环尝试就可以得到结果了。源码相对比较简单,有兴趣的朋友可以自己去了解下,这里就不贴出来了。

总结:

    结合前2节的内容,至此我们就拥有了一个强大的缓存,它可以并发且高效的数据读写、数据加载和数据移除,支持数据过期控制和持久化的平衡。在下一节中我们会在此基础上简化缓存的使用,以方便日常的调用。
 
 
 

C# 求精简用一行代码完成的多项判断 重复赋值的更多相关文章

  1. 只要一行代码求一串字符中某字符(串)出现次数,c#

    这里只要一行代码就行. static void Main(string[] args) { string str = "qwerwqr;sfdsfds;fdfdsf;dfsdfsdf;dsf ...

  2. python基础知识-8-三元和一行代码(推导式)

    python其他知识目录 1.三元运算(三目运算) 三元运算符就是在赋值变量的时候,可以直接加判断,然后赋值格式:[on_true] if [expression] else [on_false]re ...

  3. 一行代码调用实现带字段选取+条件判断+排序+分页功能的增强ORM框架

    问题:3行代码 PDF.NET是一个开源的数据开发框架,它的特点是简单.轻量.快速,易上手,而且是一个注释完善的国产开发框架,受到不少朋友的欢迎,也在我们公司的项目中多次使用.但是,PDF.NET比起 ...

  4. Sagit.Framework For IOS 开发框架入门开发教程2:一行代码实现引导页

    前言: 开篇比较简单:Sagit.Framework For IOS 开发框架入门开发教程1:框架下载与环境配置 第二篇教程之前写了一半,感觉不太好写,而且内容单纯介绍API,要说的很多,又枯燥乏味. ...

  5. python一行代码就能搞定的事情!

    打印9*9乘法表: >>> print( '\n'.join([' '.join(['%s*%s=%-2s' % (y,x,x*y) for y in range(1,x+1)]) ...

  6. Asp.Net Core 轻松学-一行代码搞定文件上传 JSONHelper

    Asp.Net Core 轻松学-一行代码搞定文件上传   前言     在 Web 应用程序开发过程中,总是无法避免涉及到文件上传,这次我们来聊一聊怎么去实现一个简单方便可复用文件上传功能:通过创建 ...

  7. python实战===一行代码就能搞定的事情!

    打印9*9乘法表: >>> print( '\n'.join([' '.join(['%s*%s=%-2s' % (y,x,x*y) for y in range(1,x+1)]) ...

  8. [WCF]缺少一行代码引发的血案

    这是今天作项目支持的发现的一个关于WCF的问题,虽然最终我只是添加了一行代码就解决了这个问题,但是整个纠错过程是痛苦的,甚至最终发现这个问题都具有偶然性.具体来说,这是一个关于如何自动为服务接口(契约 ...

  9. Android之ListView性能优化——一行代码绑定数据——万能适配器

    如下图,加入现在有一个这样的需求图,你会怎么做?作为一个初学者,之前我都是直接用SimpleAdapter结合一个Item的布局来实现的,感觉这样实现起来很方便(基本上一行代码就可以实现),而且也没有 ...

随机推荐

  1. fpga该驱动器调试dev_dbg 无输出

    近期需要调试fpga驾驶,整个是非常蛋疼.dev_dbg 我想用这个作为没有成功调试输出,它已被彻底打垮! 反射... 现在基于以下设置是不相关的打印,和网上说的有些出入,问题还得研究下. 驱动程序调 ...

  2. Jenkins master在windows

    Jenkins master在windows上安装 1 Jenkins Jenkins由以前的hudson更名而来.Jenkins的主要功能是监视重复工作的执行,例如软件工程的构建或在cron下设置的 ...

  3. javascript权威指南(6) - 对象

    JavaScript对象可以从一个称为原型的对象继承属性,这种"原型式继承"(prototypal inheritance)是JavaScript的核心特征.除了字符串.数字.tr ...

  4. 关于Java的对象、数组、String类的具体用法

    对象的行为: 1.内存分配(栈和堆的区别) 栈:保存局部变量的值(用来保存基本数据类型的值:保存类的实例的引用) 对:用来存放动态产生的数据,比如new出来的对象 2.调用方法 方法返回一个值.方法不 ...

  5. c# 替换非法字符

    保存文件的时候,文件名不允许非法字符. public string ReplaceSpecialCharacter(string str)        {            List<ch ...

  6. Ubuntu下的用户和权限(二)

    五.chown.chgrp命令 从名字就能够猜測他们是干嘛的,可是这两个命令须要root权限. chown命令的格式为:chown user:group file  中间的user : group三项 ...

  7. 【软测试】(两)计算机组成原理-cpu

    cpu,中文名称中央处理单元,central processing unit.系统的核心,用于数据的处理,算术以及逻辑运算和控制程序的运行. 组成 运算器 从字面上就能够理解到.运算器主要用来对于逻辑 ...

  8. 高性能双端js模板---simplite

    simplite是一款js实现的模板引擎,它能够完成浏览器端js模版和node服务器端js模板的数据渲染,渲染性能达到引擎的极限. 渲染性能十分突出. 支持浏览器端和node服务器端模板渲染. 它简单 ...

  9. svg的自述

    svg可缩放矢量图形(Scalable Vector Graphics). SVG 使用 XML 格式定义图像. SVG 是使用 XML 来描述二维图形和绘图程序的语言. 什么是SVG? SVG 指可 ...

  10. [连载]Java程序设计(04)---任务驱动的方法:工资结算系统

    任务:或在公司,该公司将其分为三类人员:部门经理.销售员.在发工资的时候,部门经理拿固定月薪8000元.技术人员按每小时100元领取月薪.销售人员依照500元底薪加当月销售额的4%进行提成.设计并实现 ...