主頁 > 後端開發 > golang拾遺:為什么我們需要泛型

golang拾遺:為什么我們需要泛型

2020-10-17 23:20:44 後端開發

從golang誕生起是否應該添加泛型支持就是一個熱度未曾消減的議題,泛型的支持者們認為沒有泛型的語言是不完整的,而泛型的反對者們則認為介面足以取代泛型,增加泛型只會徒增語言的復雜度,雙方各執己見,爭執不下,直到官方最終確定泛型是go2的發展路線中的重中之重,

今天我們就來看看為什么我們需要泛型,沒有泛型時我們在做什么,泛型會帶來哪些影響,泛型能拯救我們嗎?

本文索引

  • 沒有泛型的世界
    • 暴力窮舉
    • 依靠通用參考型別
    • 動態型別語言的特例
  • 動靜結合
    • 使用interface模擬泛型
    • interface會進行嚴格的型別檢查
    • 內置型別何去何從
    • 性能陷阱
    • 復合型別的迷思
    • 最后也是最重要的
  • 泛型帶來的影響,以及拯救
    • 徹底從沒有泛型的泥沼中解放
    • 泛型的代價

沒有泛型的世界

泛型最常見也是最簡單的需求就是創建一組操作相同或類似的演算法,這些演算法應該是和資料型別無關的,不管什么資料型別只要符合要求就可以操作,

看起來很簡單,我們只需要專注于演算法自身的實作,而不用操心其他細枝末節,然而現實是骨感的,想要實作型別無關演算法在沒有泛型的世界里卻是困難的,需要在許多條件中利弊取舍,

下面我們就來看看在沒有泛型的參與下我們是如何處理資料的,

暴力窮舉

這是最簡單也是最容易想到的方法,

既然演算法部分的代碼是幾乎相同的,那么就copy幾遍,然后把資料型別的地方做個修改替換,這樣的作業甚至可以用文本編輯器的代碼片段+查找替換來快速實作,比如下面的c代碼:

float a = logf(2.0f);
double b = log(2.0);

typedef struct {
    int *data;
    unsigned int max_size;
} IntQueue;

typedef struct {
    double *data;
    unsigned int max_size;
} DoubleQueue;

IntQueue* NewIntQueue(unsigned int size)
{
    IntQueue* q = (IntQueue*)malloc(sizeof(IntQueue));
    if (q == NULL) {
        return NULL;
    }
    q->max_size = size;
    q->data = https://www.cnblogs.com/apocelipes/p/(int*)malloc(size * sizeof(int));
    return q;
}

DoubleQueue* NewDoubleQueue(unsigned int size)
{
    DoubleQueue* q = (DoubleQueue*)malloc(sizeof(DoubleQueue));
    if (q == NULL) {
        return NULL;
    }
    q->max_size = size;
    q->data = (double*)malloc(size * sizeof(double));
    return q;
}

問題看上去解決了,除了修改和復查比較麻煩之外,做程式員的誰還沒有cv過呢,然而這種方法缺點很明顯:

  • 嚴重違反DRY(don't repeat yourself),資料結構的修改和擴展極其困難
  • 復制粘貼修改中可能會出現低級的人力錯誤,并且耗費精力
  • 最關鍵的一點,我們不可能針對所有型別去寫出特定的演算法,因為這些型別的數量少則5,6種,多則上不封頂,

當然,好處也不是沒有:

  • 保證了型別安全,任何型別問題都能在編譯期暴露
  • 更靈活,對于某些特定型別我們還可以做出非常細致的優化作業(比如對于bool型別我們可以使用unsigned int這個一般來說4位元組大小的型別存放32個bool值,而不是用32個bool變數消耗32位元組記憶體)

然而缺點1和缺點3給予的是致命打擊,因此通常我們不會用這種方法實作通用演算法和資料結構,(然而不幸的是golang中的math/rand就是這么實作的)

依靠通用參考型別

其實方案1還可以依靠宏來實作,linux內核就是這么做的,不過宏這個機制不是每個語言都有的,因此參考價值不是很高,

既然明確指出資料的型別不可行,那我們還有其他的辦法,比如馬上要介紹的使用通用型別參考資料,

通用的參考型別,表示它可以參考其他不同型別的資料而自身的資料型別不會改變,比如c中的void *

void *ptr = NULL;
ptr = (void*)"hello";
int a = 100;
ptr = (void*)&a;

c語言允許非函式指標的資料型別指標轉換為void *,因此我們可以用它來囊括幾乎所有的資料(函式除外),

于是Queue的代碼就會變成如下的畫風:

typedef struct {
    void *data;
    unsigned int max_size;
} Queue;

Queue* NewQueue(unsigned int size)
{
    Queue* q = (Queue*)malloc(sizeof(Queue));
    if (q == NULL) {
        return NULL;
    }
    q->max_size = size;
    q->data = https://www.cnblogs.com// 這里填什么呢?
}

代碼寫了一半發現寫不下去了?放心,這不是你的問題,在c語言里我們不能創建void型別的變數,所以我們不可能給data預先分配記憶體,

那么退一步考慮,如果引入一個java那樣的類似void*Object型別,是否就能解決記憶體分配呢?答案是否定的,假設Object大小是8位元組,如果我們放一個通常只有一位元組大小的bool進去就會有7位元組的浪費,如果我們放一個32位元組的自定義型別,那么很顯然一個Object的空間是遠遠不夠的,在c這樣的語言中我們想要使用資料就需要知道該資料的型別,想要確定型別就要先確定它的記憶體布局,而要能確定記憶體布局第一步就是要知道型別需要的記憶體空間大小,

遺憾的是通用參考型別幫我們把具體的型別資訊全部擦除了,

寫程式最重要的就是發散型的思維,如果你看到這里覺得本方案不行了的話你就太天真了,別的不說,java能用Object實作泛用容器,c也可以,秘訣很簡單,既然我們不能準確創建型別的實體,那不創建不就行了嘛,佇列本來就是負責存取資料的,創建這種作業外包給其他代碼就行了:

typedef struct {
    unsigned int max_size;
    unsigned int current;
    void **data;
} Queue;

Queue* NewQueue(unsigned int size)
{
    Queue* q = (Queue*)malloc(sizeof(Queue));
    if (q == NULL) {
        return NULL;
    }
    q->max_size = size;
    q->size = 0;
    q->data = https://www.cnblogs.com/apocelipes/p/(void **)malloc(size*sizeof(void*));
}

bool QueuePush(Queue* q, void* value)
{
    if (q == NULL || value == NULL || q->current == q->max_size-1) {
        return false;
    }

    q->data[q->current++] = value;
    return true;
}

It works! 但是我們需要佇列中的型別有特定操作呢?把操作抽象形成函式再傳遞給佇列的方法就行了,可以參考c的qsort和bsearch:

#include <sdtlib.h>

void qsort(void *base, size_t nmemb, size_t size,
                  int (*compar)(const void *, const void *));

void *bsearch(const void *key, const void *base,
                     size_t nmemb, size_t size,
                     int (*compar)(const void *, const void *));

更普遍的,你可以用鏈表去實作佇列:

typedef struct node {
   int val;
   struct node *next;
} node_t;

void enqueue(node_t **head, int val) {
   node_t *new_node = malloc(sizeof(node_t));
   if (!new_node) return;

   new_node->val = val;
   new_node->next = *head;

   *head = new_node;
}

原理同樣是將創建具體的資料的任務外包,只不過鏈表額外增加了一層node的包裝罷了,

那么這么做的好處和壞處是什么呢?

好處是我們可以遵守DRY原則了,同時還能專注于佇列本身的實作,

壞處那就有點多了:

  • 首先是型別擦除的同時沒有任何型別檢測的手段,因此型別安全無從保證,比如存進去的可以是int,取出來的時候你可以轉換成char*,程式不會給出任何警告,等你準備從這個char*里取出某個位置上的字符的時候就會引發未定義行為,從而出現許許多多奇形怪狀的bug
  • 只能存指標型別
  • 如何確定佇列里存盤資料的所有權?交給佇列管理會增加佇列實作的復雜性,不交給佇列管理就需要手動追蹤N個物件的生命周期,心智負擔很沉重,并且如果我們是存入的區域變數的指標,那么交給佇列管理就一定會導致free出現未定義行為,從代碼層面我們是幾乎不能區分一個指標是不是真的指向了堆上的內容的
  • 依舊不能避免書寫型別代碼,首先使用資料時要從void*轉換為對應型別,其次我們需要書寫如qsort例子里那樣的幫助函式,

動態型別語言的特例

在真正進入本節的主題之前,我想先介紹下什么是動態型別,什么是靜態型別,

所謂靜態型別,就是在編譯期能夠確定的變數、運算式的資料型別,換而言之,編譯期如果就能確定某個型別的記憶體布局,那么它就是靜態型別,舉個c語言的例子:

int a = 0;
const char *str = "hello generic";
double values[] = {1., 2., 3.};

上述代碼中intconst char *double[3]都是靜態型別,其中intconst char *(指標型別不受底層型別的影響,大家有著相同的大小)標準中都給出了型別所需的最小記憶體大小,而陣列型別是帶有長度的,或者是在運算式和引數傳遞中退化(decay)為指標型別,因此編譯器在編譯這些代碼的時候就能知道變數所需的記憶體大小,進而確定了其在記憶體中的布局,當然靜態型別其中還有許多細節,這里暫時不必深究,

回過來看動態型別就很好理解了,編譯期間無法確定某個變數、運算式的具體型別,這種型別就是動態的,例如下面的python代碼:

name = 'apocelipes'
name = 12345

name究竟是什么型別的變數?不知道,因為name實際上可以賦值任意的資料,我們只能在運行時的某個點做型別檢測,然后斷言name是xxx型別的,然而過了這個時間點之后name還可以賦值一個完全不同型別的資料,

好了現在我們回到正題,可能你已經猜到了,我要說的特例是什么,沒錯,因為動態型別語言實際上不關心資料的具體型別是什么,所以即使沒有泛型你也可以寫出類似泛型的代碼,而且通常它們作業得很好:

class Queue:
    def __init__(self):
        self.data = https://www.cnblogs.com/apocelipes/p/[]
    
    def push(self, value):
        self.data.append()
    
    def pop(self):
        self.data.pop()
    
    def take(self, index):
        return self.data[index]

我們既能放字串進Queue也能放整數和浮點數進去,然而這并不能稱之為泛型,使用泛型除了因為可以少寫重復的代碼,更重要的一點是可以確保代碼的型別安全,看如下例子,我們給Queue添加一個方法:

def transform(self):
    for i in range(len(self.data)):
        self.data[i] = self.data[i].upper()

我們提供了一個方法,可以將佇列中的字串從小寫轉換為大寫,問題發生了,我們的佇列不僅可以接受字串,它還可以接受數字,這時候如果我們呼叫transform方法就會發生運行時例外:AttributeError: 'int' object has no attribute 'upper',那么怎么避免問題呢?添加運行時的型別檢測就可以了,然而這樣做有兩個無法繞開的弊端:

  • 寫出了型別相關的代碼,和我們本意上想要實作型別無關的代碼結構相沖突
  • 限定了演算法只能由幾種資料型別使用,但事實上有無限多的型別可以實作upper方法,然而我們不能在型別檢查里一一列舉他們,從而導致了我們的通用演算法變為了限定演算法,

動靜結合

沒有泛型的世界實在是充滿了煎熬,不是在違反DRY原則的邊緣反復試探,就是冒著型別安全的風險激流勇進,有什么能脫離苦海的辦法嗎?

作為一門靜態強型別語言,golang提供了一個不是太完美的答案——interface,

使用interface模擬泛型

interface可以接受任何滿足要求的型別的資料,并且具有運行時的型別檢查,雙保險很大程度上提升了代碼的安全性,

一個典型的例子就是標準庫里的containers:

package list // import "container/list"

Package list implements a doubly linked list.

To iterate over a list (where l is a *List):

    for e := l.Front(); e != nil; e = e.Next() {
        // do something with e.Value
    }

type Element struct{ ... }
type List struct{ ... }
    func New() *List

type Element struct {

        // The value stored with this element.
        Value interface{}
        // Has unexported fields.
}
    Element is an element of a linked list.

func (e *Element) Next() *Element
func (e *Element) Prev() *Element

type List struct {
        // Has unexported fields.
}
    List represents a doubly linked list. The zero value for List is an empty
    list ready to use.

func New() *List
func (l *List) Back() *Element
func (l *List) Front() *Element
func (l *List) Init() *List
func (l *List) InsertAfter(v interface{}, mark *Element) *Element
func (l *List) InsertBefore(v interface{}, mark *Element) *Element
func (l *List) Len() int
func (l *List) MoveAfter(e, mark *Element)
func (l *List) MoveBefore(e, mark *Element)
func (l *List) MoveToBack(e *Element)
func (l *List) MoveToFront(e *Element)
func (l *List) PushBack(v interface{}) *Element
func (l *List) PushBackList(other *List)
...

這就是在上一大節中的方案2的型別安全強化版,介面的作業原理本文不會詳述,

但事情遠沒有結束,假設我們要對一個陣列實作indexOf的通用演算法呢?你的第一反應大概是下面這段代碼:

func IndexOfInterface(arr []interface{}, value interface{}) int {
	for i, v := range arr {
		if v == value {
			return i
		}
	}

	return -1
}

這里你會接觸到interface的第一個坑,

interface會進行嚴格的型別檢查

看看下面代碼的輸出,你能解釋為什么嗎?

func ExampleIndexOfInterface() {
    arr := []interface{}{uint(1),uint(2),uint(3),uint(4),uint(5)}
	fmt.Println(IndexOfInterface(arr, 5))
    fmt.Println(IndexOfInterface(arr, uint(5)))
    // Output:
    // -1
    // 4
}

會出現這種結果是因為interface的相等需要型別和值都相等,字面量5的值是int,所以沒有搜索到相等的值,

想要避免這種情況也不難,創建一個Comparable介面即可:

type Comparator interface {
	Compare(v interface{}) bool
}

func IndexOfComparator(arr []Comparator, value Comparator) int {
	for i,v := range arr {
		if v.Compare(value) {
			return i
		}
	}
	return -1
}

這回我們不會出錯了,因為字面量根本不能傳入函式,因為內置型別都沒實作Comparator介面,

內置型別何去何從

然而這是介面的第二個坑,我們不得不為內置型別創建包裝類和包裝方法,

假設我們還想把前文的arr直接傳入IndexOfComparator,那必定得到編譯器的抱怨:

cannot use arr (type []interface {}) as type []Comparator in argument to IndexOfComparator

為了使用這個函式我們不得不對代碼進行修改:

type MyUint uint

func (u MyUint) Compare(v interface{}) bool {
	value := v.(MyUint)
	return u == value
}

arr2 := []Comparator{MyUint(1),MyUint(2),MyUint(3),MyUint(4),MyUint(5)}
fmt.Println(IndexOfComparator(arr2, MyUint(5)))

我們希望泛型能簡化代碼,但現在卻反其道而行之了,

性能陷阱

第三個,也是被人詬病最多的,是介面帶來的性能下降,

我們對如下幾個函式做個簡單的性能測驗:

func IndexOfByReflect(arr interface{}, value interface{}) int {
	arrValue := reflect.ValueOf(arr)
	length := arrValue.Len()
	for i := 0; i < length; i++ {
		if arrValue.Index(i).Interface() == value {
			return i
		}
	}
	return -1
}

func IndexOfInterface(arr []interface{}, value interface{}) int {
	for i, v := range arr {
		if v == value {
			return i
		}
	}

	return -1
}

func IndexOfInterfacePacking(value interface{}, arr ...interface{}) int {
	for i, v := range arr {
		if v == value {
			return i
		}
	}

	return -1
}

這是測驗代碼(golang1.15.2):

const ArrLength = 500
var _arr []interface{}
var _uintArr []uint

func init() {
	_arr = make([]interface{}, ArrLength)
	_uintArr = make([]uint, ArrLength)
	for i := 0; i < ArrLength - 1; i++ {
		_uintArr[i] = uint(rand.Int() % 10 + 2)
		_arr[i] = _uintArr[i]
	}
	_arr[ArrLength - 1] = uint(1)
	_uintArr[ArrLength - 1] = uint(1)
}

func BenchmarkIndexOfInterface(b *testing.B) {
	for i := 0; i < b.N; i++ {
		IndexOfInterface(_arr, uint(1))
	}
}

func BenchmarkIndexOfInterfacePacking(b *testing.B) {
	for i := 0; i < b.N; i++ {
		IndexOfInterfacePacking(uint(1), _arr...)
	}
}

func indexOfUint(arr []uint, value uint) int {
	for i,v := range arr {
		if v == value {
			return i
		}
	}

	return -1
}

func BenchmarkIndexOfUint(b *testing.B) {
	for i := 0; i < b.N; i++ {
		indexOfUint(_uintArr, uint(1))
	}
}

func BenchmarkIndexOfByReflectInterface(b *testing.B) {
	for i := 0; i < b.N; i++ {
		IndexOfByReflect(_arr, uint(1))
	}
}

func BenchmarkIndexOfByReflectUint(b *testing.B) {
	for i := 0; i < b.N; i++ {
		IndexOfByReflect(_uintArr, uint(1))
	}
}

我們吃驚地發現,直接使用interface比原生型別慢了10倍,如果使用反射并接收原生將會慢整整100倍!

另一個使用介面的例子是比較slice是否相等,我們沒有辦法直接進行比較,需要借助輔助手段,在我以前的這篇博客有詳細的講解,性能問題同樣很顯眼,

復合型別的迷思

interface{}是介面,而[]interface{}只是一個普通的slice,復合型別中的介面是不存在協變的,所以下面的代碼是有問題的:

func work(arr []interface{}) {}

ss := []string{"hello", "golang"}
work(ss)

類似的問題其實在前文里已經出現過了,這導致我們無法用interface統一處理slice,因為interface{}并不是slice,slice的操作無法對interface使用,

為了解決這個問題,golang的sort包給出了一個頗為曲折的方案:

sort為了能處理slice,不得不包裝了常見的基本型別的slice,為了兼容自定義型別包里提供了Interface,需要你自己對自定義型別的slice進行包裝,

這實作就像是千層餅,一環套一環,即使內部的quicksort寫得再漂亮性能也是要打不少折扣的,

最后也是最重要的

對于獲取介面型別變數的值,我們需要型別斷言,然而型別斷言是運行時進行的:

var i interface{}
i = 1
s := i.(string)

這會導致panic,如果不想panic就需要第二個變數去獲取是否型別斷言成功:s, ok := i.(string)

然而真正的泛型是在編譯期就能發現這類錯誤的,而不是等到程式運行得如火如荼時突然因為panic退出,

泛型帶來的影響,以及拯救

徹底從沒有泛型的泥沼中解放

同樣是上面的IndexOf的例子,有了泛型我們可以簡單寫為:

package main

import (
	"fmt"
)

func IndexOf[T comparable](arr []T, value T) int {
    for i, v := range arr {
        if v == value {
            return i
        }
    }

    return -1
}

func main() {
	q := []uint{1,2,3,4,5}
	fmt.Println(IndexOf(q, 5))
}

comparable是go2提供的內置設施,代表所有可比較型別,你可以在這里運行上面的測驗代碼,

泛型函式會自動做型別推導,字面量可以用于初始化uint型別,所以函式正常運行,

代碼簡單干凈,而且沒有性能問題(至少官方承諾泛型的絕大部分作業會在編譯期完成),

再舉個slice判斷相等的例子:

func isEqual[T comparable](a,b []T) bool {
    if len(a) != len(b) {
        return false;
    }

    for i := range a {
        if a[i] != b[i] {
            return false
        }
    }

    return true
}

除了大幅簡化代碼之外,泛型還將給我們帶來如下改變:

  • 真正的型別安全,像isEqual([]int, []string)這樣的代碼在編譯時就會被發現并被我們修正
  • 雖然泛型也不支持協變,但slice等復合型別只要符合引數推導的規則就能被使用,限制更少
  • 沒有了介面和反射,性能自不必說,編譯期就能確定變數型別的話還可以增加代碼被優化的機會

可以說泛型是真正救人于水火,這也是泛型最終能進入go2提案的原因,

泛型的代價

最后說了這么多泛型的必要性,也該是時候談談泛型之暗了,

其實目前golang的泛型還在提案階段,雖然已經有了預覽版,但今后變數還是很多,所以這里只能針對草案簡單說說兩方面的問題,

第一個還是型別系統的割裂問題,golang使用的泛型系統比typwscript更加嚴格,any約束的型別甚至無法使用賦值運算之外的其他內置運算子,因此想要型別能比較大小的時候必定創建自定義型別和自定義的型別約束,內置型別是無法添加方法的,所以需要包裝型別,

解決這個問題不難,一條路是golang官方提供內置型別的包裝型別,并且實作java那樣的自動拆裝箱,另一條路是支持類似rust的運算子多載,例如add代表+mul代表*,這樣只需要將內置運算子進行簡單的映射即可兼容內置型別,同時又能滿足自定義型別,不過鑒于golang官方一直對運算子多載持否定態度,方案2也只能想想了,

另一個黑暗面就是泛型如何實作,現有的主流方案不是型別擦除(java,typescript),就是將泛型代碼看作模板進行實體化代碼生成(c++,rust),另外還有個另類的c#在運行時進行實體化,

目前社區仍然偏向于模板替換,采用型別字典的方案暫時無法處理泛型struct,實作也非常復雜,所以反對聲不少,如果最終敲定了模板方案,那么golang要面對的新問題就是鏈接時間過長和代碼膨脹了,一份泛型代碼可以生產數份相同的實體,這些實體需要在鏈接階段被聯結器剔除,這會導致鏈接時間爆增,代碼膨脹是老生常談的問題了,更大的二進制檔案會導致啟動更慢,代碼里的雜音更多導致cpu快取利用率的下降,

鏈接時間的優化社區有人提議可以在編譯期標記各個實體提前去重,因為golang各個代碼直接是有清晰的聯系的,不像c++檔案之間單獨編譯最終需要在鏈接階段統一處理,代碼膨脹目前沒有辦法,而且代碼膨脹會不會對性能產生影響,影響多大能否限定在可接受范圍都還是未知數,

但不管怎么說,我們都需要泛型,因為帶來的遠比失去的要多,

參考

https://colobu.com/2016/04/14/Golang-Generics-Proposal/

https://go.googlesource.com/proposal/+/refs/heads/master/design/go2draft-type-parameters.md

https://go.googlesource.com/proposal/+/refs/heads/master/design/go2draft-contracts.md

https://blog.golang.org/why-generics

https://blog.golang.org/generics-next-step

https://github.com/golang/proposal/blob/master/design/generics-implementation-gcshape.md

https://stackoverflow.com/questions/4184954/are-there-standard-queue-implementations-for-c

轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/176984.html

標籤:Go

上一篇:互聯網財富管理平臺應該怎么做?(上篇)

下一篇:Go語言基礎知識01-用Go打個招呼

標籤雲
其他(157675) Python(38076) JavaScript(25376) Java(17977) C(15215) 區塊鏈(8255) C#(7972) AI(7469) 爪哇(7425) MySQL(7132) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5869) 数组(5741) R(5409) Linux(5327) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4554) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2429) ASP.NET(2402) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) 功能(1967) .NET技术(1958) Web開發(1951) python-3.x(1918) HtmlCss(1915) 弹簧靴(1913) C++(1909) xml(1889) PostgreSQL(1872) .NETCore(1853) 谷歌表格(1846) Unity3D(1843) for循环(1842)

熱門瀏覽
  • 【C++】Microsoft C++、C 和匯編程式檔案

    ......

    uj5u.com 2020-09-10 00:57:23 more
  • 例外宣告

    相比于斷言適用于排除邏輯上不可能存在的狀態,例外通常是用于邏輯上可能發生的錯誤。 例外宣告 Item 1:當函式不可能拋出例外或不能接受拋出例外時,使用noexcept 理由 如果不打算拋出例外的話,程式就會認為無法處理這種錯誤,并且應當盡早終止,如此可以有效地阻止例外的傳播與擴散。 示例 //不可 ......

    uj5u.com 2020-09-10 00:57:27 more
  • Codeforces 1400E Clear the Multiset(貪心 + 分治)

    鏈接:https://codeforces.com/problemset/problem/1400/E 來源:Codeforces 思路:給你一個陣列,現在你可以進行兩種操作,操作1:將一段沒有 0 的區間進行減一的操作,操作2:將 i 位置上的元素歸零。最終問:將這個陣列的全部元素歸零后操作的最少 ......

    uj5u.com 2020-09-10 00:57:30 more
  • UVA11610 【Reverse Prime】

    本人看到此題沒有翻譯,就附帶了一個自己的翻譯版本 思考 這一題,它的第一個要求是找出所有 $7$ 位反向質數及其質因數的個數。 我們應該需要質數篩篩選1~$10^{7}$的所有數,這里就不慢慢介紹了。但是,重讀題,我們突然發現反向質數都是 $7$ 位,而將它反過來后的數字卻是 $6$ 位數,這就說明 ......

    uj5u.com 2020-09-10 00:57:36 more
  • 統計區間素數數量

    1 #pragma GCC optimize(2) 2 #include <bits/stdc++.h> 3 using namespace std; 4 bool isprime[1000000010]; 5 vector<int> prime; 6 inline int getlist(int ......

    uj5u.com 2020-09-10 00:57:47 more
  • C/C++編程筆記:C++中的 const 變數詳解,教你正確認識const用法

    1、C中的const 1、區域const變數存放在堆疊區中,會分配記憶體(也就是說可以通過地址間接修改變數的值)。測驗代碼如下: 運行結果: 2、全域const變數存放在只讀資料段(不能通過地址修改,會發生寫入錯誤), 默認為外部聯編,可以給其他源檔案使用(需要用extern關鍵字修飾) 運行結果: ......

    uj5u.com 2020-09-10 00:58:04 more
  • 【C++犯錯記錄】VS2019 MFC添加資源不懂如何修改資源宏ID

    1. 首先在資源視圖中,添加資源 2. 點擊新添加的資源,復制自動生成的ID 3. 在解決方案資源管理器中找到Resource.h檔案,編輯,使用整個專案搜索和替換的方式快速替換 宏宣告 4. Ctrl+Shift+F 全域搜索,點擊查找全部,然后逐個替換 5. 為什么使用搜索替換而不使用屬性視窗直 ......

    uj5u.com 2020-09-10 00:59:11 more
  • 【C++犯錯記錄】VS2019 MFC不懂的批量添加資源

    1. 打開資源頭檔案Resource.h,在其中預先定義好宏 ID(不清楚其實ID值應該設定多少,可以先新建一個相同的資源項,再在這個資源的ID值的基礎上遞增即可) 2. 在資源視圖中選中專案資源,按F7編輯資源檔案,按 ID 型別 相對路徑的形式添加 資源。(別忘了先把檔案拷貝到專案中的res檔案 ......

    uj5u.com 2020-09-10 01:00:19 more
  • C/C++編程筆記:關于C++的參考型別,專供新手入門使用

    今天要講的是C++中我最喜歡的一個用法——參考,也叫別名。 參考就是給一個變數名取一個變數名,方便我們間接地使用這個變數。我們可以給一個變數創建N個參考,這N + 1個變數共享了同一塊記憶體區域。(參考型別的變數會占用記憶體空間,占用的記憶體空間的大小和指標型別的大小是相同的。雖然參考是一個物件的別名,但 ......

    uj5u.com 2020-09-10 01:00:22 more
  • 【C/C++編程筆記】從頭開始學習C ++:初學者完整指南

    眾所周知,C ++的學習曲線陡峭,但是花時間學習這種語言將為您的職業帶來奇跡,并使您與其他開發人員區分開。您會更輕松地學習新語言,形成真正的解決問題的技能,并在編程的基礎上打下堅實的基礎。 C ++將幫助您養成良好的編程習慣(即清晰一致的編碼風格,在撰寫代碼時注釋代碼,并限制類內部的可見性),并且由 ......

    uj5u.com 2020-09-10 01:00:41 more
最新发布
  • Rust中的智能指標:Box<T> Rc<T> Arc<T> Cell<T> RefCell<T> Weak

    Rust中的智能指標是什么 智能指標(smart pointers)是一類資料結構,是擁有資料所有權和額外功能的指標。是指標的進一步發展 指標(pointer)是一個包含記憶體地址的變數的通用概念。這個地址參考,或 ” 指向”(points at)一些其 他資料 。參考以 & 符號為標志并借用了他們所 ......

    uj5u.com 2023-04-20 07:24:10 more
  • Java的值傳遞和參考傳遞

    值傳遞不會改變本身,參考傳遞(如果傳遞的值需要實體化到堆里)如果發生修改了會改變本身。 1.基本資料型別都是值傳遞 package com.example.basic; public class Test { public static void main(String[] args) { int ......

    uj5u.com 2023-04-20 07:24:04 more
  • [2]SpinalHDL教程——Scala簡單入門

    第一個 Scala 程式 shell里面輸入 $ scala scala> 1 + 1 res0: Int = 2 scala> println("Hello World!") Hello World! 檔案形式 object HelloWorld { /* 這是我的第一個 Scala 程式 * 以 ......

    uj5u.com 2023-04-20 07:23:58 more
  • 理解函式指標和回呼函式

    理解 函式指標 指向函式的指標。比如: 理解函式指標的偽代碼 void (*p)(int type, char *data); // 定義一個函式指標p void func(int type, char *data); // 宣告一個函式func p = func; // 將指標p指向函式func ......

    uj5u.com 2023-04-20 07:23:52 more
  • Django筆記二十五之資料庫函式之日期函式

    本文首發于公眾號:Hunter后端 原文鏈接:Django筆記二十五之資料庫函式之日期函式 日期函式主要介紹兩個大類,Extract() 和 Trunc() Extract() 函式作用是提取日期,比如我們可以提取一個日期欄位的年份,月份,日等資料 Trunc() 的作用則是截取,比如 2022-0 ......

    uj5u.com 2023-04-20 07:23:45 more
  • 一天吃透JVM面試八股文

    什么是JVM? JVM,全稱Java Virtual Machine(Java虛擬機),是通過在實際的計算機上仿真模擬各種計算機功能來實作的。由一套位元組碼指令集、一組暫存器、一個堆疊、一個垃圾回收堆和一個存盤方法域等組成。JVM屏蔽了與作業系統平臺相關的資訊,使得Java程式只需要生成在Java虛擬機 ......

    uj5u.com 2023-04-20 07:23:31 more
  • 使用Java接入小程式訂閱訊息!

    更新完微信服務號的模板訊息之后,我又趕緊把微信小程式的訂閱訊息給實作了!之前我一直以為微信小程式也是要企業才能申請,沒想到小程式個人就能申請。 訊息推送平臺🔥推送下發【郵件】【短信】【微信服務號】【微信小程式】【企業微信】【釘釘】等訊息型別。 https://gitee.com/zhongfuch ......

    uj5u.com 2023-04-20 07:22:59 more
  • java -- 緩沖流、轉換流、序列化流

    緩沖流 緩沖流, 也叫高效流, 按照資料型別分類: 位元組緩沖流:BufferedInputStream,BufferedOutputStream 字符緩沖流:BufferedReader,BufferedWriter 緩沖流的基本原理,是在創建流物件時,會創建一個內置的默認大小的緩沖區陣列,通過緩沖 ......

    uj5u.com 2023-04-20 07:22:49 more
  • Java-SpringBoot-Range請求頭設定實作視頻分段傳輸

    老實說,人太懶了,現在基本都不喜歡寫筆記了,但是網上有關Range請求頭的文章都太水了 下面是抄的一段StackOverflow的代碼...自己大修改過的,寫的注釋挺全的,應該直接看得懂,就不解釋了 寫的不好...只是希望能給視頻網站開發的新手一點點幫助吧. 業務場景:視頻分段傳輸、視頻多段傳輸(理 ......

    uj5u.com 2023-04-20 07:22:42 more
  • Windows 10開發教程_編程入門自學教程_菜鳥教程-免費教程分享

    教程簡介 Windows 10開發入門教程 - 從簡單的步驟了解Windows 10開發,從基本到高級概念,包括簡介,UWP,第一個應用程式,商店,XAML控制元件,資料系結,XAML性能,自適應設計,自適應UI,自適應代碼,檔案管理,SQLite資料庫,應用程式到應用程式通信,應用程式本地化,應用程式 ......

    uj5u.com 2023-04-20 07:22:35 more